diff --git a/.github/renovate.json b/.github/renovate.json index 8bafa45fd1..dd1ddc3db7 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,49 +1,76 @@ { "extends": [ - "config:base", + "config:recommended", "schedule:weekly" ], "dependencyDashboard": true, "rangeStrategy": "update-lockfile", "rebaseWhen": "conflicted", - "baseBranches": ["1.11.x"], + "baseBranches": [ + "2.1.x" + ], "packageRules": [ { - "matchPackagePatterns": ["*"], - "enabled": false + "enabled": false, + "matchPackageNames": [ + "*" + ] }, { - "matchPaths": ["+(composer.json)"], + "matchFileNames": [ + "+(composer.json)" + ], "enabled": true, - "matchBaseBranches": ["1.11.x"] + "matchBaseBranches": [ + "2.1.x" + ] }, { - "matchPaths": ["build-cs/**"], + "matchFileNames": [ + "build-cs/**" + ], "enabled": true, "groupName": "build-cs" }, { - "matchPaths": ["apigen/**"], + "matchFileNames": [ + "apigen/**" + ], "enabled": true, "groupName": "apigen" }, { - "matchPaths": ["issue-bot/**"], - "enabled": true, - "groupName": "issue-bot" + "matchFileNames": [ + "issue-bot/**" + ], + "enabled": true, + "groupName": "issue-bot" }, { - "matchPaths": ["changelog-generator/**"], + "matchFileNames": [ + "changelog-generator/**" + ], "enabled": true, "groupName": "changelog-generator" }, { - "matchPaths": ["compiler/**"], + "matchFileNames": [ + "compiler/**" + ], "enabled": true, "groupName": "compiler" }, { - "matchPaths": [".github/**"], + "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 index 36b9730690..7dc00cac9a 100644 --- a/.github/workflows/apiref.yml +++ b/.github/workflows/apiref.yml @@ -6,13 +6,16 @@ on: workflow_dispatch: push: branches: - - "1.12.x" + - "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 @@ -32,7 +35,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.1" + php-version: "8.2" - name: "Install dependencies" run: "composer install --no-interaction --no-progress" diff --git a/.github/workflows/backward-compatibility.yml b/.github/workflows/backward-compatibility.yml index 5411d5b9e3..6960e70ec8 100644 --- a/.github/workflows/backward-compatibility.yml +++ b/.github/workflows/backward-compatibility.yml @@ -6,14 +6,11 @@ on: pull_request: push: branches: - - "1.11.x" + - "2.1.x" paths: - 'src/**' - '.github/workflows/backward-compatibility.yml' -env: - COMPOSER_ROOT_VERSION: "1.11.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 @@ -35,7 +32,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.1" + php-version: "8.2" - name: "Install dependencies" run: "composer install --no-dev --no-interaction --no-progress" diff --git a/.github/workflows/build-issue-bot.yml b/.github/workflows/build-issue-bot.yml index 4769c56165..0c6904033b 100644 --- a/.github/workflows/build-issue-bot.yml +++ b/.github/workflows/build-issue-bot.yml @@ -9,14 +9,11 @@ on: - '.github/workflows/build-issue-bot.yml' push: branches: - - "1.11.x" + - "2.1.x" paths: - 'issue-bot/**' - '.github/workflows/build-issue-bot.yml' -env: - COMPOSER_ROOT_VERSION: "1.11.x-dev" - 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 diff --git a/.github/workflows/changelog-generator.yml b/.github/workflows/changelog-generator.yml index 1681e3a6e0..e2a95c8220 100644 --- a/.github/workflows/changelog-generator.yml +++ b/.github/workflows/changelog-generator.yml @@ -9,14 +9,11 @@ on: - '.github/workflows/changelog-generator.yml' push: branches: - - "1.11.x" + - "2.1.x" paths: - 'changelog-generator/**' - '.github/workflows/changelog-generator.yml' -env: - COMPOSER_ROOT_VERSION: "1.11.x-dev" - 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 @@ -36,7 +33,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.1" + php-version: "8.2" - name: "Install dependencies" run: "composer install --no-interaction --no-progress" diff --git a/.github/workflows/checksum-phar.yml b/.github/workflows/checksum-phar.yml deleted file mode 100644 index 1558436ad4..0000000000 --- a/.github/workflows/checksum-phar.yml +++ /dev/null @@ -1,127 +0,0 @@ -# https://help.github.com/en/categories/automating-your-workflow-with-github-actions - -# This workflow checks that PHAR checksum changes only when it's supposed to -# It should stay the same when the PHAR contents do not change - -name: "Check PHAR checksum" - -on: - pull_request: - paths: - - 'compiler/**' - - '.github/workflows/checksum-phar.yml' - push: - branches: - - "1.11.x" - paths: - - 'compiler/**' - - '.github/workflows/checksum-phar.yml' - -env: - COMPOSER_ROOT_VERSION: "1.11.x-dev" - -concurrency: - group: checksum-phar-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches - cancel-in-progress: true - -jobs: - check-phar-checksum: - name: "Check PHAR checksum" - - runs-on: "ubuntu-latest" - timeout-minutes: 60 - - steps: - - name: "Checkout phpstan-dist" - uses: actions/checkout@v4 - with: - repository: phpstan/phpstan - path: phpstan-dist - ref: 1.11.x - - - name: "Get info" - id: info - working-directory: phpstan-dist - run: | - echo "checksum=$(head -n 1 .phar-checksum)" >> $GITHUB_OUTPUT - echo "commit=$(tail -n 1 .phar-checksum)" >> $GITHUB_OUTPUT - - - name: "Delete phpstan-dist" - run: "rm -r phpstan-dist" - - - name: "Checkout" - uses: actions/checkout@v4 - with: - ref: ${{ steps.info.outputs.commit }} - - - name: "Checkout latest PHAR compiler" - uses: actions/checkout@v4 - with: - path: phpstan-src - ref: ${{ github.sha }} - - - name: "Delete old compiler" - run: "rm -r compiler" - - - name: "Move new compiler" - run: "mv phpstan-src/compiler/ ." - - - name: "Delete phpstan-src" - run: "rm -r phpstan-src" - - - name: "Change and commit README.md" - run: | - echo Testing > README.md - git config --global user.name "phpstan-bot" - git config --global user.email "ondrej+phpstanbot@mirtes.cz" - git commit -a -m 'Changed README' - - - name: "Install PHP" - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: "8.1" - extensions: mbstring, intl - - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress" - - - name: "Install compiler dependencies" - run: "composer install --no-interaction --no-progress --working-dir=compiler" - - # same steps as in phar.yml - - - name: "Prepare for PHAR compilation" - working-directory: "compiler" - run: "php bin/prepare" - - - name: "Set autoloader suffix" - run: "composer config autoloader-suffix PHPStanChecksum" - - - name: "Composer dump" - run: "composer install --no-interaction --no-progress" - env: - COMPOSER_ROOT_VERSION: "1.11.x-dev" - - - name: "Compile PHAR for checksum" - working-directory: "compiler/build" - run: "php box.phar compile --no-parallel" - env: - PHAR_CHECKSUM: "1" - COMPOSER_ROOT_VERSION: "1.11.x-dev" - - - name: "Re-sign PHAR" - run: "php compiler/build/resign.php tmp/phpstan.phar" - - - name: "Unset autoloader suffix" - run: "composer config autoloader-suffix --unset" - - - name: "Save checksum" - id: "new_checksum" - run: echo "md5=$(md5sum tmp/phpstan.phar | cut -d' ' -f1)" >> $GITHUB_OUTPUT - - - name: "Assert checksum" - run: | - checksum=${{ steps.info.outputs.checksum }} - new_checksum=${{ steps.new_checksum.outputs.md5 }} - [[ "$checksum" == "$new_checksum" ]]; diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index aeb7efb6fc..9f9e1376c6 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -11,51 +11,18 @@ on: - 'issue-bot/**' push: branches: - - "1.11.x" + - "2.1.x" paths-ignore: - 'compiler/**' - 'apigen/**' - 'changelog-generator/**' - 'issue-bot/**' -env: - COMPOSER_ROOT_VERSION: "1.11.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-php-parser-e2e: - name: "Result cache PHP-Parser E2E test" - - runs-on: ${{ matrix.operating-system }} - timeout-minutes: 60 - - strategy: - fail-fast: false - matrix: - operating-system: [ubuntu-latest, windows-latest] - - steps: - - name: "Checkout" - uses: actions/checkout@v4 - - - name: "Install PHP" - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: "8.1" - extensions: mbstring - ini-values: memory_limit=256M - - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress" - - - name: "Tests" - run: | - git clone https://github.com/nikic/PHP-Parser.git tests/e2e/PHP-Parser && git -C tests/e2e/PHP-Parser checkout v3.1.5 && composer install --working-dir tests/e2e/PHP-Parser && vendor/bin/phpunit tests/e2e/ResultCacheEndToEndTest.php - result-cache-e2e-tests: name: "Result cache E2E tests" runs-on: ubuntu-latest @@ -170,20 +137,52 @@ jobs: ../../bin/phpstan -vvv - script: | cd e2e/env-parameter - export PHPSTAN_SCOPE_CLASS=MyTestScope - ACTUAL=$(../../bin/phpstan dump-parameters -c phpstan.neon --json -l 9 | jq --raw-output '.scopeClass') - [[ "$ACTUAL" == "MyTestScope" ]]; + 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 2>&1) - grep 'Result cache might not behave correctly' <<< "$OUTPUT" - grep 'ResultCache8E2E\\CustomRule' <<< "$OUTPUT" + 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/ @@ -192,52 +191,55 @@ jobs: cd e2e/trait-caching ../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ patch -b data/TraitOne.php < TraitOne.patch - OUTPUT=$(../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ || true) + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/") echo "$OUTPUT" - ../bashunit -a line_count 1 "$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=$(../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ || true) + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/") echo "$OUTPUT" - ../bashunit -a line_count 1 "$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=$(../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ || true) + 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 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=$(../../bin/phpstan analyse -c ignore.neon 2>&1 || true) + 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" + ../bashunit -a contains 'tests" is neither a directory, nor a file path, nor a fnmatch pattern.' "$OUTPUT" - script: | cd e2e/bad-exclude-paths - OUTPUT=$(../../bin/phpstan analyse -c phpneon.php 2>&1 || true) + 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" + ../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 excludePaths.neon 2>&1 || true) + 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" + ../bashunit -a contains 'tests" is neither a directory, nor a file path, nor a fnmatch pattern.' "$OUTPUT" - script: | cd e2e/bad-exclude-paths - OUTPUT=$(../../bin/phpstan analyse -c phpneon2.php 2>&1 || true) + 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" + ../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) @@ -251,6 +253,35 @@ jobs: cd e2e/bad-exclude-paths OUTPUT=$(../../bin/phpstan analyse -c ignoreReportUnmatchedFalse.neon) echo "$OUTPUT" + - script: | + cd e2e/bug-11826 + composer install + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan") + echo "$OUTPUT" + ../bashunit -a contains 'Child process error (exit code 255): PHP Fatal error' "$OUTPUT" + ../bashunit -a contains 'Result is incomplete because of severe errors.' "$OUTPUT" + - script: | + cd e2e/bug-11857 + composer install + ../../bin/phpstan + - script: | + cd e2e/result-cache-meta-extension + composer install + ../../bin/phpstan -vvv + ../../bin/phpstan -vvv --fail-without-result-cache + echo 'modified-hash' > hash.txt + OUTPUT=$(../bashunit -a exit_code "2" "../../bin/phpstan -vvv --fail-without-result-cache") + echo "$OUTPUT" + ../bashunit -a matches "Note: Using configuration file .+phpstan.neon." "$OUTPUT" + ../bashunit -a contains 'Result cache not used because the metadata do not match: metaExtensions' "$OUTPUT" + - script: | + cd e2e/bug-12606 + export CONFIGTEST=test + ../../bin/phpstan + - script: | + cd e2e/ignore-error-extension + composer install + ../../bin/phpstan steps: - name: "Checkout" @@ -260,7 +291,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.1" + php-version: "8.2" extensions: mbstring ini-values: memory_limit=256M @@ -271,7 +302,7 @@ jobs: 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.13.0" + run: "curl -s https://bashunit.typeddevs.com/install.sh | bash -s e2e/ 0.22.0" - name: "Test" run: "${{ matrix.script }}" @@ -282,6 +313,7 @@ jobs: timeout-minutes: 60 strategy: + fail-fast: false matrix: include: - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/timecop.php -c tests/e2e/data/empty.neon tests/e2e/data/timecop.php" @@ -311,6 +343,55 @@ jobs: cd e2e/discussion-11362 composer install ../../bin/phpstan + - script: | + cd e2e/bug-11819 + ../../bin/phpstan + - script: | + cd e2e/composer-and-phpstan-version-config + composer install --ignore-platform-reqs + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-max-version + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-max-version + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-open-end-version + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-version-v5 + composer install --ignore-platform-reqs + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-version-v7 + composer install --ignore-platform-reqs + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-version + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-no-versions + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-version-config-invalid + OUTPUT=$(../bashunit -a exit_code "1" ../../bin/phpstan) + echo "$OUTPUT" + ../bashunit -a contains 'Invalid configuration' "$OUTPUT" + ../bashunit -a contains 'Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.' "$OUTPUT" + - script: | + cd e2e/composer-version-config-patch + composer install --ignore-platform-reqs + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-version-config + composer install + ../../bin/phpstan analyze test.php --level=0 steps: - name: "Checkout" @@ -320,12 +401,15 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.1" + php-version: "8.2" tools: ${{ matrix.tools }} extensions: ${{ matrix.extensions }} - name: "Install dependencies" run: "composer install --no-interaction --no-progress" + - name: "Install bashunit" + run: "curl -s https://bashunit.typeddevs.com/install.sh | bash -s e2e/ 0.22.0" + - name: "Test" run: ${{ matrix.script }} diff --git a/.github/workflows/issue-bot.yml b/.github/workflows/issue-bot.yml index df03ddeb72..2366222087 100644 --- a/.github/workflows/issue-bot.yml +++ b/.github/workflows/issue-bot.yml @@ -11,15 +11,12 @@ on: - 'changelog-generator/**' push: branches: - - "1.12.x" + - "2.1.x" paths-ignore: - 'compiler/**' - 'apigen/**' - 'changelog-generator/**' -env: - COMPOSER_ROOT_VERSION: "1.12.x-dev" - concurrency: group: run-issue-bot-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches cancel-in-progress: true @@ -51,9 +48,9 @@ jobs: uses: actions/cache@v4 with: path: ./issue-bot/tmp - key: "issue-bot-download-v6-${{ github.run_id }}" + key: "issue-bot-download-v7-${{ github.run_id }}" restore-keys: | - issue-bot-download-v6- + issue-bot-download-v7- - name: "Download data" working-directory: "issue-bot" @@ -100,7 +97,7 @@ jobs: working-directory: "issue-bot" run: "composer install --no-interaction --no-progress" - - uses: Wandalen/wretry.action@v3.5.0 + - uses: Wandalen/wretry.action@v3.8.0 with: action: actions/download-artifact@v4 with: | @@ -167,7 +164,7 @@ jobs: - name: "Evaluate results - push" working-directory: "issue-bot" - if: "github.repository_owner == 'phpstan' && github.ref == 'refs/heads/1.12.x'" + 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 }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 14fbfbc500..e42c6f23cf 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,10 +6,7 @@ on: pull_request: push: branches: - - "1.11.x" - -env: - COMPOSER_ROOT_VERSION: "1.11.x-dev" + - "2.1.x" concurrency: group: lint-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches @@ -25,13 +22,13 @@ jobs: fail-fast: false matrix: php-version: - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" - "8.2" - "8.3" + - "8.4" + - "8.5" steps: - name: "Checkout" @@ -43,15 +40,22 @@ 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" - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - name: "Transform source code" - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3' - run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" + 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: "Validate Composer" + run: "composer validate" - name: "Lint" run: "make lint" @@ -70,7 +74,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.1" + php-version: "8.2" - name: "Validate Composer" run: "composer validate" @@ -98,7 +102,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.1" + php-version: "8.3" - name: "Install dependencies" run: "composer install --no-interaction --no-progress" @@ -120,7 +124,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.3" + php-version: "8.4" - name: "Install dependencies" run: "composer install --no-interaction --no-progress" diff --git a/.github/workflows/merge-maintained-branch.yml b/.github/workflows/merge-maintained-branch.yml index 4b609e26e2..0ac13c5f68 100644 --- a/.github/workflows/merge-maintained-branch.yml +++ b/.github/workflows/merge-maintained-branch.yml @@ -5,7 +5,7 @@ name: Merge maintained branch on: push: branches: - - "1.11.x" + - "1.12.x" jobs: merge: @@ -20,5 +20,5 @@ jobs: with: github_token: "${{ secrets.PHPSTAN_BOT_TOKEN }}" source_ref: ${{ github.ref }} - target_branch: '1.12.x' + target_branch: '2.1.x' commit_message_template: 'Merge branch {source_ref} into {target_branch}' diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index af601aa93b..e12c7fe986 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -6,9 +6,9 @@ on: pull_request: push: branches: - - "1.11.x" + - "2.1.x" tags: - - '1.11.*' + - '2.1.*' concurrency: group: phar-${{ github.ref }} # will be canceled on subsequent pushes in both branches and pull requests @@ -23,6 +23,7 @@ jobs: outputs: checksum: ${{ steps.checksum.outputs.md5 }} + compiler_changed: ${{ steps.changes.outputs.compiler }} steps: - name: "Checkout" @@ -34,12 +35,16 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.1" + php-version: "8.2" extensions: mbstring, intl - name: "Install dependencies" run: "composer install --no-interaction --no-progress" + # 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 --working-dir=compiler" @@ -49,15 +54,22 @@ jobs: - name: "Compiler PHPStan" working-directory: "compiler" - run: "../bin/phpstan analyse -l 8 src tests" + run: "vendor/bin/phpstan analyse -l 8 src tests" - 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" + working-directory: "compiler/box" + run: "composer install" + - name: "Compile PHAR" working-directory: "compiler/build" - run: "php box.phar compile --no-parallel" + run: "php ../box/vendor/bin/box compile --no-parallel" - uses: actions/upload-artifact@v4 with: @@ -77,14 +89,14 @@ jobs: - name: "Composer dump" run: "composer install --no-interaction --no-progress" env: - COMPOSER_ROOT_VERSION: "1.11.x-dev" + COMPOSER_ROOT_VERSION: "2.1.x-dev" - name: "Compile PHAR for checksum" working-directory: "compiler/build" - run: "php box.phar compile --no-parallel" + run: "php ../box/vendor/bin/box compile --no-parallel" env: PHAR_CHECKSUM: "1" - COMPOSER_ROOT_VERSION: "1.11.x-dev" + COMPOSER_ROOT_VERSION: "2.1.x-dev" - name: "Re-sign PHAR" run: "php compiler/build/resign.php tmp/phpstan.phar" @@ -104,33 +116,168 @@ jobs: - 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@1.11.x + uses: phpstan/phpstan/.github/workflows/integration-tests.yml@2.1.x with: - ref: 1.11.x + 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@1.11.x + uses: phpstan/phpstan/.github/workflows/extension-tests.yml@2.1.x with: - ref: 1.11.x + 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@1.11.x + uses: phpstan/phpstan/.github/workflows/other-tests.yml@2.1.x with: - ref: 1.11.x + 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" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Install Box dependencies" + working-directory: "compiler/box" + run: "composer install" + + - 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/1.11.x' || startsWith(github.ref, 'refs/tags/'))" + 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 @@ -152,7 +299,7 @@ jobs: repository: phpstan/phpstan path: phpstan-dist token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - ref: 1.11.x + ref: 2.1.x - name: "Get previous pushed dist commit" id: previous-commit diff --git a/.github/workflows/pr-base-on-previous-branch.yml b/.github/workflows/pr-base-on-previous-branch.yml index 84015d215a..34ef71bb83 100644 --- a/.github/workflows/pr-base-on-previous-branch.yml +++ b/.github/workflows/pr-base-on-previous-branch.yml @@ -7,7 +7,7 @@ on: types: - opened branches: - - '1.12.x' + - '2.2.x' jobs: @@ -19,6 +19,6 @@ jobs: - name: Comment PR uses: peter-evans/create-or-update-comment@v4 with: - body: "You've opened the pull request against the latest branch 1.12.x. If your code is relevant on 1.11.x and you want it to be released sooner, please rebase your pull request and change its target to 1.11.x." + 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/reflection-golden-test.yml b/.github/workflows/reflection-golden-test.yml index b3db1c394c..44cceae778 100644 --- a/.github/workflows/reflection-golden-test.yml +++ b/.github/workflows/reflection-golden-test.yml @@ -11,7 +11,7 @@ on: - 'issue-bot/**' push: branches: - - "1.11.x" + - "2.1.x" paths-ignore: - 'compiler/**' - 'apigen/**' @@ -19,7 +19,6 @@ on: - 'issue-bot/**' env: - COMPOSER_ROOT_VERSION: "1.11.x-dev" REFLECTION_GOLDEN_TEST_FILE: "/tmp/reflection-golden.test" REFLECTION_GOLDEN_SYMBOLS_FILE: "/tmp/reflection-golden-symbols.txt" @@ -65,15 +64,13 @@ jobs: fail-fast: false matrix: php-version: - - "7.3" - - "7.4" - - "8.0" - - "8.1" - "8.2" - "8.3" + - "8.4" + - "8.5" steps: - - uses: Wandalen/wretry.action@v3.5.0 + - uses: Wandalen/wretry.action@v3.8.0 with: action: actions/download-artifact@v4 with: | @@ -100,11 +97,6 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Transform source code" - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3' - shell: bash - run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" - - name: "Dump previous reflection data" run: "php tests/generate-reflection-test.php" @@ -119,10 +111,5 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Transform source code" - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3' - shell: bash - run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" - - name: "Reflection golden test" run: "make tests-golden-reflection || true" diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml index 8fd37ce761..b2f810732c 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "1.11.x" + - "2.1.x" jobs: typos: @@ -18,6 +18,6 @@ jobs: uses: actions/checkout@v4 - name: "Check for typos" - uses: "crate-ci/typos@v1.23.6" + 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 390bfa4b7e..a49e5e7623 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -9,14 +9,11 @@ on: - 'apigen/**' push: branches: - - "1.11.x" + - "2.1.x" paths-ignore: - 'compiler/**' - 'apigen/**' -env: - COMPOSER_ROOT_VERSION: "1.11.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 @@ -31,13 +28,13 @@ jobs: fail-fast: false matrix: php-version: - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" - "8.2" - "8.3" + - "8.4" + - "8.5" operating-system: [ubuntu-latest, windows-latest] steps: @@ -52,22 +49,21 @@ jobs: ini-file: development extensions: mbstring + - name: "Downgrade PHPUnit" + 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" + - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - name: "Transform source code" - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3' - shell: bash - run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" - - - name: "Paratest patch" - if: matrix.php-version == '7.2' - run: composer config extra.patches.brianium/paratest --json --merge '["patches/paratest.patch"]' + if: matrix.php-version == '7.4' || matrix.php-version == '8.0' || matrix.php-version == '8.1' shell: bash - - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.2' - run: "composer require --dev phpunit/phpunit:^8.5.31 brianium/paratest:^4.0 composer/semver:^1.2 --update-with-dependencies --ignore-platform-reqs" + 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: "PHPStan" run: "make phpstan" @@ -82,9 +78,10 @@ jobs: fail-fast: false matrix: php-version: - - "8.1" - "8.2" - "8.3" + - "8.4" + - "8.5" steps: - name: "Checkout" @@ -132,7 +129,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.1" + php-version: "8.2" ini-file: development - name: "Install dependencies" @@ -158,7 +155,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.1" + php-version: "8.2" ini-file: development - name: "Install dependencies" diff --git a/.github/workflows/tests-levels-matrix.php b/.github/workflows/tests-levels-matrix.php index 1344e8e81e..d5dbc90b82 100644 --- a/.github/workflows/tests-levels-matrix.php +++ b/.github/workflows/tests-levels-matrix.php @@ -1,6 +1,6 @@ testCaseClass as $testCaseClass) { - foreach($testCaseClass->testCaseMethod as $testCaseMethod) { - if ((string) $testCaseMethod['groups'] !== 'levels') { - continue; - } - - $testCaseName = (string) $testCaseMethod['id']; +foreach($simpleXml->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'; + [$className, $testName] = explode('::', $testCaseName, 2); + $fileName = 'tests/' . str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php'; - $filter = str_replace('\\', '\\\\', $testCaseName); + $filter = str_replace('\\', '\\\\', $testCaseName); - $testFilters[] = sprintf("%s --filter %s", escapeshellarg($fileName), escapeshellarg($filter)); + $testFilters[] = sprintf("%s --filter %s", escapeshellarg($fileName), escapeshellarg($filter)); + } } } diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f1a49d48f3..c431ecee52 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,16 +11,13 @@ on: - 'issue-bot/**' push: branches: - - "1.11.x" + - "2.1.x" paths-ignore: - 'compiler/**' - 'apigen/**' - 'changelog-generator/**' - 'issue-bot/**' -env: - COMPOSER_ROOT_VERSION: "1.11.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 @@ -35,12 +32,10 @@ jobs: fail-fast: false matrix: php-version: - - "7.3" - - "7.4" - - "8.0" - - "8.1" - "8.2" - "8.3" + - "8.4" + - "8.5" operating-system: [ ubuntu-latest, windows-latest ] steps: @@ -60,11 +55,6 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Transform source code" - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3' - shell: bash - run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" - - name: "Tests" run: "make tests" @@ -86,7 +76,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.1" + php-version: "8.2" tools: pecl extensions: ds,mbstring ini-file: development @@ -117,8 +107,8 @@ jobs: ini-file: development ini-values: memory_limit=1G - - name: "Install PHPUnit 10.x" - run: "composer remove --dev brianium/paratest && composer require --dev --with-all-dependencies phpunit/phpunit:^10" + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" - id: set-matrix run: echo "matrix=$(php .github/workflows/tests-levels-matrix.php)" >> $GITHUB_OUTPUT @@ -158,7 +148,7 @@ jobs: - name: "Tests" run: "${{ matrix.script }}" - tests-old-phpunit: + tests-with-old-phpunit: name: "Tests with old PHPUnit" runs-on: ${{ matrix.operating-system }} timeout-minutes: 60 @@ -167,8 +157,10 @@ jobs: fail-fast: false matrix: php-version: - - "7.2" - operating-system: [ ubuntu-latest ] + - "7.4" + - "8.0" + - "8.1" + operating-system: [ ubuntu-latest, windows-latest ] steps: - name: "Checkout" @@ -184,19 +176,23 @@ jobs: ini-file: development ini-values: memory_limit=2G + - name: "Downgrade PHPUnit" + shell: bash + run: "composer require --dev phpunit/phpunit:^9.6 sebastian/diff:^4.0 --update-with-dependencies --ignore-platform-reqs" + - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Transform source code" + - name: "Downgrade PHPUnit with Paratest" shell: bash - run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" + 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: "Paratest patch" - run: composer config extra.patches.brianium/paratest --json --merge '["patches/paratest.patch"]' + - name: "Transform source code" shell: bash - - - name: "Downgrade PHPUnit" - run: "composer require --dev phpunit/phpunit:^8.5.31 brianium/paratest:^4.0 composer/semver:^1.2 --update-with-dependencies --ignore-platform-reqs" + 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: "Tests" - run: "make tests-coverage" + run: "make tests" diff --git a/.github/workflows/update-phpstorm-stubs.yml b/.github/workflows/update-phpstorm-stubs.yml index f0fc56a9b1..cd528fcfb6 100644 --- a/.github/workflows/update-phpstorm-stubs.yml +++ b/.github/workflows/update-phpstorm-stubs.yml @@ -16,14 +16,14 @@ jobs: - name: "Checkout" uses: actions/checkout@v4 with: - ref: 1.11.x + ref: 2.1.x fetch-depth: '0' token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.1" + php-version: "8.2" - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - name: "Checkout stubs" diff --git a/.typos.toml b/.typos.toml index 2687f239ed..78c99b1d76 100644 --- a/.typos.toml +++ b/.typos.toml @@ -8,3 +8,7 @@ ignore-hidden = false # Known typos NonRemoveableTypeTrait = "NonRemoveableTypeTrait" supportsLessOverridenParametersWithVariadic = "supportsLessOverridenParametersWithVariadic" + +[default.extend-words] +# override false-positives +Excluder = "Excluder" diff --git a/LICENSE b/LICENSE index 7c0f2b7b69..e5f34e607a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2016 Ondřej Mirtes +Copyright (c) 2025 PHPStan s.r.o. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index 6670f1cea2..da87ae01aa 100644 --- a/Makefile +++ b/Makefile @@ -2,23 +2,17 @@ build: cs tests phpstan -tests: - php vendor/bin/paratest --runner WrapperRunner --no-coverage +tests: install-paratest + XDEBUG_MODE=off php tests/vendor/bin/paratest --runner WrapperRunner --no-coverage -tests-integration: - php vendor/bin/paratest --runner WrapperRunner --no-coverage --group exec - -tests-levels: - php vendor/bin/paratest --runner WrapperRunner --no-coverage --group levels - -tests-coverage: - php vendor/bin/paratest --runner WrapperRunner +tests-integration: install-paratest + php tests/vendor/bin/paratest --runner WrapperRunner --no-coverage --group exec tests-golden-reflection: - php vendor/bin/paratest --runner WrapperRunner --no-coverage tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php + 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 \ @@ -43,6 +37,7 @@ lint: --exclude tests/PHPStan/Rules/Functions/data/arrow-function-nullsafe-by-ref.php \ --exclude tests/PHPStan/Levels/data/namedArguments.php \ --exclude tests/PHPStan/Rules/Keywords/data/continue-break.php \ + --exclude tests/PHPStan/Rules/Keywords/data/continue-break-property-hook.php \ --exclude tests/PHPStan/Rules/Properties/data/invalid-callable-property-type.php \ --exclude tests/PHPStan/Rules/Properties/data/properties-in-interface.php \ --exclude tests/PHPStan/Rules/Properties/data/read-only-property.php \ @@ -54,6 +49,7 @@ lint: --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 \ @@ -75,13 +71,50 @@ lint: --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 \ src tests +install-paratest: + composer install --working-dir tests + cs: - composer install --working-dir build-cs && php build-cs/vendor/bin/phpcs + composer install --working-dir build-cs && XDEBUG_MODE=off php build-cs/vendor/bin/phpcs cs-fix: - php build-cs/vendor/bin/phpcbf + XDEBUG_MODE=off php build-cs/vendor/bin/phpcbf phpstan: php bin/phpstan clear-result-cache -q && php -d memory_limit=448M bin/phpstan @@ -89,6 +122,9 @@ phpstan: phpstan-result-cache: php -d memory_limit=448M bin/phpstan +phpstan-fix: + php -d memory_limit=448M bin/phpstan --fix + phpstan-generate-baseline: php -d memory_limit=448M bin/phpstan --generate-baseline diff --git a/README.md b/README.md index 6d2e16963c..e817604542 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,6 @@ Any contributions are welcome. composer install ``` -If you encounter dependency problem, try using `export COMPOSER_ROOT_VERSION=1.11.x-dev` - 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 diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000000..1b76768ec4 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,338 @@ +Upgrading from PHPStan 1.x to 2.0 +================================= + +## PHP version requirements + +PHPStan now requires PHP 7.4 or newer to run. + +## Upgrading guide for end users + +The best way to get ready for upgrade to PHPStan 2.0 is to update to the **latest PHPStan 1.12 release** +and enable [**Bleeding Edge**](https://phpstan.org/blog/what-is-bleeding-edge). This will enable the new rules and behaviours that 2.0 turns on for all users. + +Also make sure to install and enable [`phpstan/phpstan-deprecation-rules`](https://github.com/phpstan/phpstan-deprecation-rules). + +Once you get to a green build with no deprecations showed on latest PHPStan 1.12.x with Bleeding Edge enabled, you can update all your related PHPStan dependencies to 2.0 in `composer.json`: + +```json +"require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-doctrine": "^2.0", + "phpstan/phpstan-nette": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpstan/phpstan-symfony": "^2.0", + "phpstan/phpstan-webmozart-assert": "^2.0", + ... +} +``` + +Don't forget to update [3rd party PHPStan extensions](https://phpstan.org/user-guide/extension-library) as well. + +After changing your `composer.json`, run `composer update 'phpstan/*' -W`. + +It's up to you whether you go through the new reported errors or if you just put them all to the [baseline](https://phpstan.org/user-guide/baseline) ;) Everyone who's on PHPStan 1.12 should be able to upgrade to PHPStan 2.0. + +### Noteworthy changes to code analysis + +* [**Enhancements in handling parameters passed by reference**](https://phpstan.org/blog/enhancements-in-handling-parameters-passed-by-reference) +* [**Validate inline PHPDoc `@var` tag type**](https://phpstan.org/blog/phpstan-1-10-comes-with-lie-detector#validate-inline-phpdoc-%40var-tag-type) +* [**List type enforced**](https://phpstan.org/blog/phpstan-1-9-0-with-phpdoc-asserts-list-type#list-type) +* **Always `true` conditions always reported**: previously reported only with phpstan-strict-rules, this is now always reported. + +### Removed option `checkMissingIterableValueType` + +It's strongly recommended to add the missing array typehints. + +If you want to continue ignoring missing typehints from arrays, add `missingType.iterableValue` error identifier to your `ignoreErrors`: + +```neon +parameters: + ignoreErrors: + - + identifier: missingType.iterableValue +``` + +### Removed option `checkGenericClassInNonGenericObjectType` + +It's strongly recommended to add the missing generic typehints. + +If you want to continue ignoring missing typehints from generics, add `missingType.generics` error identifier to your `ignoreErrors`: + +```neon +parameters: + ignoreErrors: + - + identifier: missingType.generics +``` + +### Removed `checkAlwaysTrue*` options + +These options have been removed because PHPStan now always behaves as if these were set to `true`: + +* `checkAlwaysTrueCheckTypeFunctionCall` +* `checkAlwaysTrueInstanceof` +* `checkAlwaysTrueStrictComparison` +* `checkAlwaysTrueLooseComparison` + +### Removed option `excludes_analyse` + +It has been replaced with [`excludePaths`](https://phpstan.org/user-guide/ignoring-errors#excluding-whole-files). + +### Paths in `excludePaths` and `ignoreErrors` have to be a valid file path or a fnmatch pattern + +If you are excluding a file path that might not exist but you still want to have it in `excludePaths`, append `(?)`: + +```neon +parameters: + excludePaths: + - tests/*/data/* + - src/broken + - node_modules (?) # optional path, might not exist +``` + +If you have the same situation in `ignoreErrors` (ignoring an error in a path that might not exist), use `reportUnmatchedIgnoredErrors: false`. + +```neon +parameters: + reportUnmatchedIgnoredErrors: false +``` + +Appending `(?)` in `ignoreErrors` is not supported. + +### Changes in 1st party PHPStan extensions + +* [phpstan-doctrine](https://github.com/phpstan/phpstan-doctrine) + * Removed config parameter `searchOtherMethodsForQueryBuilderBeginning` (extension now behaves as when this was set to `true`) + * Removed config parameter `queryBuilderFastAlgorithm` (extension now behaves as when this was set to `false`) +* [phpstan-symfony](https://github.com/phpstan/phpstan-symfony) + * Removed legacy options with `_` in the name + * `container_xml_path` -> use `containerXmlPath` + * `constant_hassers` -> use `constantHassers` + * `console_application_loader` -> use `consoleApplicationLoader` + +### Minor backward compatibility breaks + +* Removed unused config parameter `cache.nodesByFileCountMax` +* Removed unused config parameter `memoryLimitFile` +* Removed unused feature toggle `disableRuntimeReflectionProvider` +* Removed unused config parameter `staticReflectionClassNamePatterns` +* Remove `fixerTmpDir` config parameter, use `pro.tmpDir` instead +* Remove `tempResultCachePath` config parameter, use `resultCachePath` instead +* `additionalConfigFiles` config parameter must be a list + +## Upgrading guide for extension developers + +> [!NOTE] +> Please switch to PHPStan 2.0 in a new major version of your extension. It's not feasible to try to support both PHPStan 1.x and PHPStan 2.x with the same extension code. +> +> You can definitely get closer to supporting PHPStan 2.0 without increasing major version by solving reported deprecations and other issues by analysing your extension code with PHPStan & phpstan-deprecation-rules & Bleeding Edge, but the final leap and solving backward incompatibilities should be done by requiring `"phpstan/phpstan": "^2.0"` in your `composer.json`, and releasing a new major version. + +### PHPStan now uses nikic/php-parser v5 + +See [UPGRADING](https://github.com/nikic/PHP-Parser/blob/master/UPGRADE-5.0.md) guide for PHP-Parser. + +The most notable change is how `throw` statement is represented. Previously, `throw` statements like `throw $e;` were represented using the `Stmt\Throw_` class, while uses inside other expressions (such as `$x ?? throw $e`) used the `Expr\Throw_` class. + +Now, `throw $e;` is represented as a `Stmt\Expression` that contains an `Expr\Throw_`. The +`Stmt\Throw_` class has been removed. + +### PHPStan now uses phpstan/phpdoc-parser v2 + +See [UPGRADING](https://github.com/phpstan/phpdoc-parser/blob/2.0.x/UPGRADING.md) guide for phpstan/phpdoc-parser. + +### Returning plain strings as errors no longer supported, use RuleErrorBuilder + +Identifiers are also required in custom rules. + +Learn more: [Using RuleErrorBuilder to enrich reported errors in custom rules](https://phpstan.org/blog/using-rule-error-builder) + +**Before**: + +```php +return ['My error']; +``` + +**After**: + +```php +return [ + RuleErrorBuilder::message('My error') + ->identifier('my.error') + ->build(), +]; +``` + +### Deprecate various `instanceof *Type` in favour of new methods on `Type` interface + +Learn more: [Why Is instanceof *Type Wrong and Getting Deprecated?](https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated) + +### Removed deprecated `ParametersAcceptorSelector::selectSingle()` + +Use [`ParametersAcceptorSelector::selectFromArgs()`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ParametersAcceptorSelector.html#_selectFromArgs) instead. It should be used in most places where `selectSingle()` was previously used, like dynamic return type extensions. + +**Before**: + +```php +$defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); +``` + +**After**: + +```php +$defaultReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants() +)->getReturnType(); +``` + +If you're analysing function or method body itself and you're using one of the following methods, ask for `getParameters()` and `getReturnType()` directly on the reflection object: + +* [InClassMethodNode::getMethodReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.InClassMethodNode.html) +* [InFunctionNode::getFunctionReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.InFunctionNode.html) +* [FunctionReturnStatementsNode::getFunctionReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.FunctionReturnStatementsNode.html) +* [MethodReturnStatementsNode::getMethodReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.MethodReturnStatementsNode.html) +* [Scope::getFunction()](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.Scope.html#_getFunction) + +**Before**: + +```php +$function = $node->getFunctionReflection(); +$returnType = ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(); +``` + +**After**: + +```php +$returnType = $node->getFunctionReflection()->getReturnType(); +``` + +### Changed `TypeSpecifier::create()` and `SpecifiedTypes` constructor parameters + +[`PHPStan\Analyser\TypeSpecifier::create()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.TypeSpecifier.html#_create) now accepts (all parameters are required): + +* `Expr $expr` +* `Type $type` +* `TypeSpecifierContext $context` +* `Scope $scope` + +If you want to change `$overwrite` or `$rootExpr` (previous parameters also used to be accepted by this method), call `setAlwaysOverwriteTypes()` and `setRootExpr()` on [`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) (object returned by `TypeSpecifier::create()`). These methods return a new object (SpecifiedTypes is immutable). + +[`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) constructor now accepts: + +* `array $sureTypes` +* `array $sureNotTypes` + +If you want to change `$overwrite` or `$rootExpr` (previous parameters also used to be accepted by the constructor), call `setAlwaysOverwriteTypes()` and `setRootExpr()`. These methods return a new object (SpecifiedTypes is immutable). + +### `ConstantArrayType` no longer extends `ArrayType` + +`Type::getArrays()` now returns `list`. + +Using `$type instanceof ArrayType` is [being deprecated anyway](https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated) so the impact of this change should be minimal. + +### Changed `TypeSpecifier::specifyTypesInCondition()` + +This method now longer accepts `Expr $rootExpr`. If you want to change it, call `setRootExpr()` on [`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) (object returned by `TypeSpecifier::specifyTypesInCondition()`). `setRootExpr()` method returns a new object (SpecifiedTypes is immutable). + +### Node attributes `parent`, `previous`, `next` are no longer available + +Learn more: https://phpstan.org/blog/preprocessing-ast-for-custom-rules + +### Removed config parameter `scopeClass` + +As a replacement you can implement [`PHPStan\Type\ExpressionTypeResolverExtension`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.ExpressionTypeResolverExtension.html) interface instead and register it as a service. + +### Removed `PHPStan\Broker\Broker` + +Use [`PHPStan\Reflection\ReflectionProvider`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ReflectionProvider.html) instead. + +`BrokerAwareExtension` was also removed. Ask for `ReflectionProvider` in the extension constructor instead. + +Instead of `PHPStanTestCase::createBroker()`, call `PHPStanTestCase::createReflectionProvider()`. + +### List type is enabled for everyone + +Removed static methods from `AccessoryArrayListType` class: + +* `isListTypeEnabled()` +* `setListTypeEnabled()` +* `intersectWith()` + +Instead of `AccessoryArrayListType::intersectWith($type)`, do `TypeCombinator::intersect($type, new AccessoryArrayListType())`. + +### Minor backward compatibility breaks + +* Classes that were previously `@final` were made `final` +* Parameter `$callableParameters` of [`MutatingScope::enterAnonymousFunction()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.MutatingScope.html#_enterAnonymousFunction) and [`enterArrowFunction()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.MutatingScope.html#_enterArrowFunction) made required +* Parameter `StatementContext $context` of [`NodeScopeResolver::processStmtNodes()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.NodeScopeResolver.html#_processStmtNodes) made required +* ClassPropertiesNode - remove `$extensions` parameter from [`getUninitializedProperties()`](https://apiref.phpstan.org/2.0.x/PHPStan.Node.ClassPropertiesNode.html#_getUninitializedProperties) +* `Type::getSmallerType()`, `Type::getSmallerOrEqualType()`, `Type::getGreaterType()`, `Type::getGreaterOrEqualType()`, `Type::isSmallerThan()`, `Type::isSmallerThanOrEqual()` now require [`PhpVersion`](https://apiref.phpstan.org/2.0.x/PHPStan.Php.PhpVersion.html) as argument. +* `CompoundType::isGreaterThan()`, `CompoundType::isGreaterThanOrEqual()` now require [`PhpVersion`](https://apiref.phpstan.org/2.0.x/PHPStan.Php.PhpVersion.html) as argument. +* Removed `ReflectionProvider::supportsAnonymousClasses()` (all reflection providers support anonymous classes) +* Remove `ArrayType::generalizeKeys()` +* Remove `ArrayType::count()`, use `Type::getArraySize()` instead +* Remove `ArrayType::castToArrayKeyType()`, `Type::toArrayKey()` instead +* Remove `UnionType::pickTypes()`, use `pickFromTypes()` instead +* Remove `RegexArrayShapeMatcher::matchType()`, use `matchExpr()` instead +* Remove unused `PHPStanTestCase::$useStaticReflectionProvider` +* Remove `PHPStanTestCase::getReflectors()`, use `getReflector()` instead +* Remove `ClassReflection::getFileNameWithPhpDocs()`, use `getFileName()` instead +* Remove `AnalysisResult::getInternalErrors()`, use `getInternalErrorObjects()` instead +* Remove `ConstantReflection::getValue()`, use `getValueExpr()` instead. To get `Type` from `Expr`, use `Scope::getType()` or `InitializerExprTypeResolver::getType()` +* Remove `PropertyTag::getType()`, use `getReadableType()` / `getWritableType()` instead +* Remove `GenericTypeVariableResolver`, use [`Type::getTemplateType()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getTemplateType) instead +* Rename `Type::isClassStringType()` to `Type::isClassString()` +* Remove `Scope::isSpecified()`, use `hasExpressionType()` instead +* Remove `ConstantArrayType::isEmpty()`, use `isIterableAtLeastOnce()->no()` instead +* Remove `ConstantArrayType::getNextAutoIndex()` +* Removed methods from `ConstantArrayType` - `getFirst*Type` and `getLast*Type` + * Use `getFirstIterable*Type` and `getLastIterable*Type` instead +* Remove `ConstantArrayType::generalizeToArray()` +* Remove `ConstantArrayType::findTypeAndMethodName()`, use `findTypeAndMethodNames()` instead +* Remove `ConstantArrayType::removeLast()`, use [`Type::popArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_popArray) instead +* Remove `ConstantArrayType::removeFirst()`, use [`Type::shiftArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_shiftArray) instead +* Remove `ConstantArrayType::reverse()`, use [`Type::reverseArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_reverseArray) instead +* Remove `ConstantArrayType::chunk()`, use [`Type::chunkArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_chunkArray) instead +* Remove `ConstantArrayType::slice()`, use [`Type::sliceArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_sliceArray) instead +* Made `TypeUtils` thinner by removing methods: + * Remove `TypeUtils::getArrays()` and `getAnyArrays()`, use [`Type::getArrays()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getArrays) instead + * Remove `TypeUtils::getConstantArrays()` and `getOldConstantArrays()`, use [`Type::getConstantArrays()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantArrays) instead + * Remove `TypeUtils::getConstantStrings()`, use [`Type::getConstantStrings()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantStrings) instead + * Remove `TypeUtils::getConstantTypes()` and `getAnyConstantTypes()`, use [`Type::isConstantValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantValue) or [`Type::generalize()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_generalize) + * Remove `TypeUtils::generalizeType()`, use [`Type::generalize()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_generalize) instead + * Remove `TypeUtils::getDirectClassNames()`, use [`Type::getObjectClassNames()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getObjectClassNames) instead + * Remove `TypeUtils::getConstantScalars()`, use [`Type::isConstantScalarValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantScalarValue) or [`Type::getConstantScalarTypes()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantScalarTypes) instead + * Remove `TypeUtils::getEnumCaseObjects()`, use [`Type::getEnumCases()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getEnumCases) instead + * Remove `TypeUtils::containsCallable()`, use [`Type::isCallable()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isCallable) instead +* Removed `Scope::doNotTreatPhpDocTypesAsCertain()`, use `getNativeType()` instead +* Parameter `$isList` in `ConstantArrayType` constructor can only be `TrinaryLogic`, no longer `bool` +* Parameter `$nextAutoIndexes` in `ConstantArrayType` constructor can only be `non-empty-list`, no longer `int` +* Remove `ConstantType` interface, use [`Type::isConstantValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantValue) instead +* `acceptsNamedArguments()` in `FunctionReflection`, `ExtendedMethodReflection` and `CallableParametersAcceptor` interfaces returns `TrinaryLogic` instead of `bool` +* Remove `FunctionReflection::isFinal()` +* [`Type::getProperty()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getProperty) now returns [`ExtendedPropertyReflection`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ExtendedPropertyReflection.html) +* Remove `__set_state()` on objects that should not be serialized in cache +* Parameter `$selfClass` of [`TypehintHelper::decideTypeFromReflection()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.TypehintHelper.html#_decideTypeFromReflection) no longer accepts `string` +* `LevelsTestCase::dataTopics()` data provider made static +* `PHPStan\Node\Printer\Printer` no longer autowired as `PhpParser\PrettyPrinter\Standard`, use `PHPStan\Node\Printer\Printer` in the typehint +* Remove `Type::acceptsWithReason()`, `Type:accepts()` return type changed from `TrinaryLogic` to [`AcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html) +* Remove `CompoundType::isAcceptedWithReasonBy()`, `CompoundType::isAcceptedBy()` return type changed from `TrinaryLogic` to [`AcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html) +Remove `Type::isSuperTypeOfWithReason()`, `Type:isSuperTypeOf()` return type changed from `TrinaryLogic` to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html) +* Remove `CompoundType::isSubTypeOfWithReasonBy()`, `CompoundType::isSubTypeOf()` return type changed from `TrinaryLogic` to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html) +* Remove `TemplateType::isValidVarianceWithReason()`, changed `TemplateType::isValidVariance()` return type to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html) +* `RuleLevelHelper::accepts()` return type changed from `bool` to [`RuleLevelHelperAcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html) +* Changes around `ClassConstantReflection` + * Class `ClassConstantReflection` removed from BC promise, renamed to `RealClassConstantReflection` + * Interface `ConstantReflection` renamed to `ClassConstantReflection` + * Added more methods around PHPDoc types and native types to the (new) `ClassConstantReflection` + * Interface `GlobalConstantReflection` renamed to `ConstantReflection` +* Renamed interfaces and classes from `*WithPhpDocs` to `Extended*` + * `ParametersAcceptorWithPhpDocs` -> `ExtendedParametersAcceptor` + * `ParameterReflectionWithPhpDocs` -> `ExtendedParameterReflection` + * `FunctionVariantWithPhpDocs` -> `ExtendedFunctionVariant` +* `ClassPropertyNode::getNativeType()` return type changed from AST node to `Type|null` +* Class `PHPStan\Node\ClassMethod` (accessible from `ClassMethodsNode`) is no longer an AST node + * Call `PHPStan\Node\ClassMethod::getNode()` to access the original AST node diff --git a/apigen/composer.json b/apigen/composer.json index cac161def9..827002329a 100644 --- a/apigen/composer.json +++ b/apigen/composer.json @@ -3,7 +3,7 @@ "php": "^8.1" }, "require-dev": { - "apigen/apigen": "dev-master#52f74870943620f96c669d730596fc1091117441" + "apigen/apigen": "dev-master#aa151a961053d20e46d2c7da65cbb03c130d12ff" }, "autoload": { "psr-4": { diff --git a/apigen/composer.lock b/apigen/composer.lock index 5f296fa17b..36e6073f5e 100644 --- a/apigen/composer.lock +++ b/apigen/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cca72dc10f8d1df2104049e381d62fcb", + "content-hash": "f25de5a945d9862c3ba649cab28f3e61", "packages": [], "packages-dev": [ { @@ -13,39 +13,44 @@ "source": { "type": "git", "url": "https://github.com/ApiGen/ApiGen.git", - "reference": "52f74870943620f96c669d730596fc1091117441" + "reference": "aa151a961053d20e46d2c7da65cbb03c130d12ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ApiGen/ApiGen/zipball/52f74870943620f96c669d730596fc1091117441", - "reference": "52f74870943620f96c669d730596fc1091117441", + "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": "^2022.1", + "jetbrains/phpstorm-stubs": "^2024.2", "latte/latte": "^3.0", "league/commonmark": "^2.3", - "nette/di": "^3.0", - "nette/finder": "^2.5", + "nette/di": "^3.1", + "nette/finder": "^3.0", "nette/schema": "^1.2", - "nette/utils": "^3.2", - "nikic/php-parser": "^4.14", + "nette/utils": "^4.0", + "nikic/php-parser": "^5.3", "php": "^8.1", - "phpstan/php-8-stubs": "^0.3.9", - "phpstan/phpdoc-parser": "^1.5", - "symfony/console": "^6.0" + "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": { - "phpstan/phpstan": "^1.7", + "nette/neon": "^3.4", + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.9", "tracy/tracy": "^2.9" }, - "suggest": { - "ext-pcntl": "for multiprocess rendering" - }, "default-branch": true, "bin": [ "bin/apigen" @@ -83,20 +88,20 @@ "issues": "https://github.com/ApiGen/ApiGen/issues", "source": "https://github.com/ApiGen/ApiGen/tree/master" }, - "time": "2022-07-10T16:43:45+00:00" + "time": "2025-02-21T14:27:46+00:00" }, { "name": "dflydev/dot-access-data", - "version": "v3.0.1", + "version": "v3.0.3", "source": { "type": "git", "url": "https://github.com/dflydev/dflydev-dot-access-data.git", - "reference": "0992cc19268b259a39e86f296da5f0677841f42c" + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/0992cc19268b259a39e86f296da5f0677841f42c", - "reference": "0992cc19268b259a39e86f296da5f0677841f42c", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", "shasum": "" }, "require": { @@ -107,7 +112,7 @@ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", "scrutinizer/ocular": "1.6.0", "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "^3.14" + "vimeo/psalm": "^4.0.0" }, "type": "library", "extra": { @@ -156,30 +161,29 @@ ], "support": { "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", - "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.1" + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" }, - "time": "2021-08-13T13:06:58+00:00" + "time": "2024-07-08T12:26:09+00:00" }, { "name": "jetbrains/phpstorm-stubs", - "version": "v2022.1", + "version": "v2024.3", "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "066fa5b3cd989b9c4fb1793d5ad20af5ab0e2b3c" + "reference": "0e82bdfe850c71857ee4ee3501ed82a9fc5d043c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/066fa5b3cd989b9c4fb1793d5ad20af5ab0e2b3c", - "reference": "066fa5b3cd989b9c4fb1793d5ad20af5ab0e2b3c", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/0e82bdfe850c71857ee4ee3501ed82a9fc5d043c", + "reference": "0e82bdfe850c71857ee4ee3501ed82a9fc5d043c", "shasum": "" }, "require-dev": { - "friendsofphp/php-cs-fixer": "@stable", - "nikic/php-parser": "@stable", - "php": "^8.0", - "phpdocumentor/reflection-docblock": "@stable", - "phpunit/phpunit": "@stable" + "friendsofphp/php-cs-fixer": "v3.64.0", + "nikic/php-parser": "v5.3.1", + "phpdocumentor/reflection-docblock": "5.6.0", + "phpunit/phpunit": "11.4.3" }, "type": "library", "autoload": { @@ -204,42 +208,44 @@ "type" ], "support": { - "source": "https://github.com/JetBrains/phpstorm-stubs/tree/v2022.1" + "source": "https://github.com/JetBrains/phpstorm-stubs/tree/v2024.3" }, - "time": "2022-03-08T07:40:50+00:00" + "time": "2024-12-14T08:03:12+00:00" }, { "name": "latte/latte", - "version": "v3.0.2", + "version": "v3.0.23", "source": { "type": "git", "url": "https://github.com/nette/latte.git", - "reference": "c3eee2e4e2c21cdf9f9c158c4bfa6150625457e1" + "reference": "3198a4e336a2a1e535924af11d9a63fbf1650836" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/latte/zipball/c3eee2e4e2c21cdf9f9c158c4bfa6150625457e1", - "reference": "c3eee2e4e2c21cdf9f9c158c4bfa6150625457e1", + "url": "https://api.github.com/repos/nette/latte/zipball/3198a4e336a2a1e535924af11d9a63fbf1650836", + "reference": "3198a4e336a2a1e535924af11d9a63fbf1650836", "shasum": "" }, "require": { "ext-json": "*", "ext-tokenizer": "*", - "php": ">=8.0 <8.2" + "php": "8.0 - 8.4" }, "conflict": { - "nette/application": "<2.4.1" + "nette/application": "<3.1.7", + "nette/caching": "<3.1.4" }, "require-dev": { - "nette/php-generator": "^3.3.4", - "nette/tester": "^2.0", - "nette/utils": "^3.0", - "phpstan/phpstan": "^1", - "tracy/tracy": "^2.3" + "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" @@ -254,6 +260,9 @@ } }, "autoload": { + "psr-4": { + "Latte\\": "src/Latte" + }, "classmap": [ "src/" ] @@ -288,22 +297,22 @@ ], "support": { "issues": "https://github.com/nette/latte/issues", - "source": "https://github.com/nette/latte/tree/v3.0.2" + "source": "https://github.com/nette/latte/tree/v3.0.23" }, - "time": "2022-06-15T13:42:57+00:00" + "time": "2025-07-17T01:01:46+00:00" }, { "name": "league/commonmark", - "version": "2.3.3", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "0da1dca5781dd3cfddbe328224d9a7a62571addc" + "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/0da1dca5781dd3cfddbe328224d9a7a62571addc", - "reference": "0da1dca5781dd3cfddbe328224d9a7a62571addc", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", + "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", "shasum": "" }, "require": { @@ -316,22 +325,23 @@ }, "require-dev": { "cebe/markdown": "^1.0", - "commonmark/cmark": "0.30.0", - "commonmark/commonmark.js": "0.30.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", + "michelf/php-markdown": "^1.4 || ^2.0", "nyholm/psr7": "^1.5", - "phpstan/phpstan": "^0.12.88 || ^1.0.0", - "phpunit/phpunit": "^9.5.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", - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0", - "unleashedtech/php-coding-standard": "^3.1", - "vimeo/psalm": "^4.7.3" + "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" @@ -339,7 +349,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.4-dev" + "dev-main": "2.8-dev" } }, "autoload": { @@ -396,20 +406,20 @@ "type": "tidelift" } ], - "time": "2022-06-07T21:28:26+00:00" + "time": "2025-05-05T12:20:28+00:00" }, { "name": "league/config", - "version": "v1.1.1", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/thephpleague/config.git", - "reference": "a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e" + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/config/zipball/a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e", - "reference": "a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", "shasum": "" }, "require": { @@ -418,7 +428,7 @@ "php": "^7.4 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.90", + "phpstan/phpstan": "^1.8.2", "phpunit/phpunit": "^9.5.5", "scrutinizer/ocular": "^1.8.1", "unleashedtech/php-coding-standard": "^3.1", @@ -478,43 +488,41 @@ "type": "github" } ], - "time": "2021-08-14T12:15:32+00:00" + "time": "2022-12-11T20:36:23+00:00" }, { "name": "nette/di", - "version": "v3.0.13", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/nette/di.git", - "reference": "9878f2958a0a804b08430dbc719a52e493022739" + "reference": "57f923a7af32435b6e4921c0adbc70c619625a17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/di/zipball/9878f2958a0a804b08430dbc719a52e493022739", - "reference": "9878f2958a0a804b08430dbc719a52e493022739", + "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": "^3.5.4 || ^4.0", - "nette/robot-loader": "^3.2", - "nette/schema": "^1.1", - "nette/utils": "^3.1.6", - "php": ">=7.1 <8.2" - }, - "conflict": { - "nette/bootstrap": "<3.0" + "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.2", - "phpstan/phpstan": "^0.12", - "tracy/tracy": "^2.3" + "nette/tester": "^2.5.2", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -551,47 +559,33 @@ ], "support": { "issues": "https://github.com/nette/di/issues", - "source": "https://github.com/nette/di/tree/v3.0.13" + "source": "https://github.com/nette/di/tree/v3.2.4" }, - "time": "2022-03-10T02:43:04+00:00" + "time": "2025-01-10T04:57:37+00:00" }, { "name": "nette/finder", - "version": "v2.5.3", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/nette/finder.git", - "reference": "64dc25b7929b731e72a1bc84a9e57727f5d5d3e8" + "reference": "027395c638637de95c8e9fad49a7c51249404ed2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/finder/zipball/64dc25b7929b731e72a1bc84a9e57727f5d5d3e8", - "reference": "64dc25b7929b731e72a1bc84a9e57727f5d5d3e8", + "url": "https://api.github.com/repos/nette/finder/zipball/027395c638637de95c8e9fad49a7c51249404ed2", + "reference": "027395c638637de95c8e9fad49a7c51249404ed2", "shasum": "" }, "require": { - "nette/utils": "^2.4 || ^3.0", - "php": ">=7.1" - }, - "conflict": { - "nette/nette": "<2.2" - }, - "require-dev": { - "nette/tester": "^2.0", - "phpstan/phpstan": "^0.12", - "tracy/tracy": "^2.3" + "nette/utils": "^4.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "3.0-dev" } }, - "autoload": { - "classmap": [ - "src/" - ] - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause", @@ -618,31 +612,31 @@ ], "support": { "issues": "https://github.com/nette/finder/issues", - "source": "https://github.com/nette/finder/tree/v2.5.3" + "source": "https://github.com/nette/finder/tree/v3.0.0" }, - "time": "2021-12-12T17:43:24+00:00" + "time": "2022-12-14T17:05:54+00:00" }, { "name": "nette/neon", - "version": "v3.3.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/nette/neon.git", - "reference": "22e384da162fab42961d48eb06c06d3ad0c11b95" + "reference": "3411aa86b104e2d5b7e760da4600865ead963c3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/neon/zipball/22e384da162fab42961d48eb06c06d3ad0c11b95", - "reference": "22e384da162fab42961d48eb06c06d3ad0c11b95", + "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", + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", "tracy/tracy": "^2.7" }, "bin": [ @@ -651,7 +645,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.3-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -686,31 +680,32 @@ ], "support": { "issues": "https://github.com/nette/neon/issues", - "source": "https://github.com/nette/neon/tree/v3.3.3" + "source": "https://github.com/nette/neon/tree/v3.4.4" }, - "time": "2022-03-10T02:04:26+00:00" + "time": "2024-10-04T22:00:08+00:00" }, { "name": "nette/php-generator", - "version": "v4.0.2", + "version": "v4.1.8", "source": { "type": "git", "url": "https://github.com/nette/php-generator.git", - "reference": "f19b7975c7c4d729be5b64fce7eb72f0d4aac6fc" + "reference": "42806049a7774a2bd316c958f5dcf01c6b5c56fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/php-generator/zipball/f19b7975c7c4d729be5b64fce7eb72f0d4aac6fc", - "reference": "f19b7975c7c4d729be5b64fce7eb72f0d4aac6fc", + "url": "https://api.github.com/repos/nette/php-generator/zipball/42806049a7774a2bd316c958f5dcf01c6b5c56fa", + "reference": "42806049a7774a2bd316c958f5dcf01c6b5c56fa", "shasum": "" }, "require": { - "nette/utils": "^3.2.7 || ^4.0", - "php": ">=8.0 <8.2" + "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.13", + "nikic/php-parser": "^4.18 || ^5.0", "phpstan/phpstan": "^1.0", "tracy/tracy": "^2.8" }, @@ -720,7 +715,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -744,7 +739,7 @@ "homepage": "https://nette.org/contributors" } ], - "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.1 features.", + "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.4 features.", "homepage": "https://nette.org", "keywords": [ "code", @@ -754,39 +749,38 @@ ], "support": { "issues": "https://github.com/nette/php-generator/issues", - "source": "https://github.com/nette/php-generator/tree/v4.0.2" + "source": "https://github.com/nette/php-generator/tree/v4.1.8" }, - "time": "2022-06-17T12:20:08+00:00" + "time": "2025-03-31T00:29:29+00:00" }, { "name": "nette/robot-loader", - "version": "v3.4.1", + "version": "v4.0.3", "source": { "type": "git", "url": "https://github.com/nette/robot-loader.git", - "reference": "e2adc334cb958164c050f485d99c44c430f51fe2" + "reference": "45d67753fb4865bb718e9a6c9be69cc9470137b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/robot-loader/zipball/e2adc334cb958164c050f485d99c44c430f51fe2", - "reference": "e2adc334cb958164c050f485d99c44c430f51fe2", + "url": "https://api.github.com/repos/nette/robot-loader/zipball/45d67753fb4865bb718e9a6c9be69cc9470137b7", + "reference": "45d67753fb4865bb718e9a6c9be69cc9470137b7", "shasum": "" }, "require": { "ext-tokenizer": "*", - "nette/finder": "^2.5 || ^3.0", - "nette/utils": "^3.0", - "php": ">=7.1" + "nette/utils": "^4.0", + "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.9" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -821,37 +815,37 @@ ], "support": { "issues": "https://github.com/nette/robot-loader/issues", - "source": "https://github.com/nette/robot-loader/tree/v3.4.1" + "source": "https://github.com/nette/robot-loader/tree/v4.0.3" }, - "time": "2021-08-25T15:53:54+00:00" + "time": "2024-06-18T20:26:39+00:00" }, { "name": "nette/schema", - "version": "v1.2.2", + "version": "v1.3.2", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df" + "reference": "da801d52f0354f70a638673c4a0f04e16529431d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/9a39cef03a5b34c7de64f551538cbba05c2be5df", - "reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df", + "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d", "shasum": "" }, "require": { - "nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", - "php": ">=7.1 <8.2" + "nette/utils": "^4.0", + "php": "8.1 - 8.4" }, "require-dev": { - "nette/tester": "^2.3 || ^2.4", - "phpstan/phpstan-nette": "^0.12", - "tracy/tracy": "^2.7" + "nette/tester": "^2.5.2", + "phpstan/phpstan-nette": "^1.0", + "tracy/tracy": "^2.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -883,34 +877,36 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.2.2" + "source": "https://github.com/nette/schema/tree/v1.3.2" }, - "time": "2021-10-15T11:40:02+00:00" + "time": "2024-10-06T23:10:23+00:00" }, { "name": "nette/utils", - "version": "v3.2.7", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "0af4e3de4df9f1543534beab255ccf459e7a2c99" + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/0af4e3de4df9f1543534beab255ccf459e7a2c99", - "reference": "0af4e3de4df9f1543534beab255ccf459e7a2c99", + "url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2", + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2", "shasum": "" }, "require": { - "php": ">=7.2 <8.2" + "php": "8.0 - 8.4" }, "conflict": { - "nette/di": "<3.0.6" + "nette/finder": "<3", + "nette/schema": "<1.2.2" }, "require-dev": { - "nette/tester": "~2.0", + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.5", "phpstan/phpstan": "^1.0", - "tracy/tracy": "^2.3" + "tracy/tracy": "^2.9" }, "suggest": { "ext-gd": "to use Image", @@ -918,13 +914,12 @@ "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" + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -968,31 +963,33 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v3.2.7" + "source": "https://github.com/nette/utils/tree/v4.0.7" }, - "time": "2022-01-24T11:29:14+00:00" + "time": "2025-06-03T04:55:08+00:00" }, { "name": "nikic/php-parser", - "version": "v4.14.0", + "version": "v5.5.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1" + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/34bea19b6e03d8153165d8f30bba4c3be86184c1", - "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", "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" @@ -1000,7 +997,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -1024,22 +1021,22 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.14.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" }, - "time": "2022-05-31T20:59:12+00:00" + "time": "2025-05-31T08:24:38+00:00" }, { "name": "phpstan/php-8-stubs", - "version": "0.3.17", + "version": "0.4.14", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "2be8adf73c7034f78ca109bb5bb6cb14ea04a49f" + "reference": "66b0ffe2d4dba18f12ffee663cdbe31640819e7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/2be8adf73c7034f78ca109bb5bb6cb14ea04a49f", - "reference": "2be8adf73c7034f78ca109bb5bb6cb14ea04a49f", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/66b0ffe2d4dba18f12ffee663cdbe31640819e7a", + "reference": "66b0ffe2d4dba18f12ffee663cdbe31640819e7a", "shasum": "" }, "type": "library", @@ -1056,28 +1053,30 @@ "description": "PHP stubs extracted from php-src", "support": { "issues": "https://github.com/phpstan/php-8-stubs/issues", - "source": "https://github.com/phpstan/php-8-stubs/tree/0.3.17" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.14" }, - "time": "2022-06-23T00:14:01+00:00" + "time": "2025-06-16T07:31:03+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.6.4", + "version": "1.33.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "135607f9ccc297d6923d49c2bcf309f509413215" + "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/135607f9ccc297d6923d49c2bcf309f509413215", - "reference": "135607f9ccc297d6923d49c2bcf309f509413215", + "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", @@ -1101,9 +1100,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.6.4" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.33.0" }, - "time": "2022-06-26T13:09:08+00:00" + "time": "2024-10-13T11:25:22+00:00" }, { "name": "psr/container", @@ -1210,24 +1209,24 @@ }, { "name": "symfony/console", - "version": "v6.1.2", + "version": "v6.4.23", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "7a86c1c42fbcb69b59768504c7bca1d3767760b7" + "reference": "9056771b8eca08d026cd3280deeec3cfd99c4d93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/7a86c1c42fbcb69b59768504c7bca1d3767760b7", - "reference": "7a86c1c42fbcb69b59768504c7bca1d3767760b7", + "url": "https://api.github.com/repos/symfony/console/zipball/9056771b8eca08d026cd3280deeec3cfd99c4d93", + "reference": "9056771b8eca08d026cd3280deeec3cfd99c4d93", "shasum": "" }, "require": { "php": ">=8.1", - "symfony/deprecation-contracts": "^2.1|^3", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/string": "^5.4|^6.0" + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" }, "conflict": { "symfony/dependency-injection": "<5.4", @@ -1241,18 +1240,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/lock": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0", - "symfony/var-dumper": "^5.4|^6.0" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" + "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": { @@ -1281,12 +1278,12 @@ "homepage": "https://symfony.com", "keywords": [ "cli", - "command line", + "command-line", "console", "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.1.2" + "source": "https://github.com/symfony/console/tree/v6.4.23" }, "funding": [ { @@ -1302,20 +1299,20 @@ "type": "tidelift" } ], - "time": "2022-06-26T13:01:30+00:00" + "time": "2025-06-27T19:37:22+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.1.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", - "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -1323,12 +1320,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.1-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.6-dev" } }, "autoload": { @@ -1353,7 +1350,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -1369,118 +1366,33 @@ "type": "tidelift" } ], - "time": "2022-02-25T11:15:52+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.26.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.26-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.26.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "433d05519ce6990bf3530fba6957499d327395c2" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2", - "reference": "433d05519ce6990bf3530fba6957499d327395c2", + "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.26-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1516,7 +1428,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -1532,36 +1444,33 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.26.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "219aa369ceff116e673852dce47c3a41794c14bd" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", - "reference": "219aa369ceff116e673852dce47c3a41794c14bd", + "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.26-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1600,90 +1509,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-05-24T11:49:31+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.26.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.26-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -1699,123 +1525,38 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" - }, - { - "name": "symfony/polyfill-php80", - "version": "v1.26.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", - "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.26-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-05-10T07:21:04+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.1.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239" + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/925e713fe8fcacf6bc05e936edd8dd5441a21239", - "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", "shasum": "" }, "require": { "php": ">=8.1", - "psr/container": "^2.0" + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" }, - "suggest": { - "symfony/service-implementation": "" - }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.1-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" } }, "autoload": { @@ -1851,7 +1592,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.1.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" }, "funding": [ { @@ -1867,37 +1608,39 @@ "type": "tidelift" } ], - "time": "2022-05-30T19:18:58+00:00" + "time": "2025-04-25T09:37:31+00:00" }, { "name": "symfony/string", - "version": "v6.1.2", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "1903f2879875280c5af944625e8246d81c2f0604" + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/1903f2879875280c5af944625e8246d81c2f0604", - "reference": "1903f2879875280c5af944625e8246d81c2f0604", + "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", "shasum": "" }, "require": { - "php": ">=8.1", + "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.0" + "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^5.4|^6.0", - "symfony/http-client": "^5.4|^6.0", - "symfony/translation-contracts": "^2.0|^3.0", - "symfony/var-exporter": "^5.4|^6.0" + "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": { @@ -1936,7 +1679,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.1.2" + "source": "https://github.com/symfony/string/tree/v7.3.0" }, "funding": [ { @@ -1952,7 +1695,7 @@ "type": "tidelift" } ], - "time": "2022-06-26T16:35:04+00:00" + "time": "2025-04-20T20:19:01+00:00" } ], "aliases": [], @@ -1965,6 +1708,6 @@ "platform": { "php": "^8.1" }, - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index 838d2ecd5c..d429d11b59 100644 --- a/bin/functionMetadata_original.php +++ b/bin/functionMetadata_original.php @@ -70,8 +70,6 @@ 'chown' => ['hasSideEffects' => true], 'copy' => ['hasSideEffects' => true], 'count' => ['hasSideEffects' => false], - 'connection_aborted' => ['hasSideEffects' => true], - 'connection_status' => ['hasSideEffects' => true], 'error_log' => ['hasSideEffects' => true], 'fclose' => ['hasSideEffects' => true], 'fflush' => ['hasSideEffects' => true], @@ -79,7 +77,6 @@ 'fgetcsv' => ['hasSideEffects' => true], 'fgets' => ['hasSideEffects' => true], 'fgetss' => ['hasSideEffects' => true], - 'file_get_contents' => ['hasSideEffects' => true], 'file_put_contents' => ['hasSideEffects' => true], 'flock' => ['hasSideEffects' => true], 'fopen' => ['hasSideEffects' => true], @@ -98,6 +95,18 @@ '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], @@ -121,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], @@ -155,6 +170,9 @@ 'DateTimeImmutable::getTimestamp' => ['hasSideEffects' => false], 'DateTimeImmutable::getTimezone' => ['hasSideEffects' => false], + 'SplDoublyLinkedList::pop' => ['hasSideEffects' => true], + 'SplDoublyLinkedList::shift' => ['hasSideEffects' => true], + 'SplFileObject::fflush' => ['hasSideEffects' => true], 'SplFileObject::fgetc' => ['hasSideEffects' => true], 'SplFileObject::fgetcsv' => ['hasSideEffects' => true], @@ -168,6 +186,24 @@ '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-function-metadata.php b/bin/generate-function-metadata.php index f6f5131a53..d161d374e4 100755 --- a/bin/generate-function-metadata.php +++ b/bin/generate-function-metadata.php @@ -16,7 +16,7 @@ (function (): void { require_once __DIR__ . '/../vendor/autoload.php'; - $parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7); + $parser = (new ParserFactory())->createForNewestSupportedVersion(); $finder = new Finder(); $finder->in(__DIR__ . '/../vendor/jetbrains/phpstorm-stubs')->files()->name('*.php'); @@ -25,18 +25,63 @@ /** @var string[] */ public array $functions = []; + /** @var list */ + public array $impureFunctions = []; + /** @var string[] */ public array $methods = []; public function enterNode(Node $node) { if ($node instanceof Node\Stmt\Function_) { + assert(isset($node->namespacedName)); + $functionName = $node->namespacedName->toLowerString(); + foreach ($node->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { - if ($attr->name->toString() === Pure::class) { - $this->functions[] = $node->namespacedName->toLowerString(); + if ($attr->name->toString() !== Pure::class) { + continue; + } + + // The following functions have side effects, but their state is managed within the PHPStan scope: + if (in_array($functionName, [ + 'stat', + 'lstat', + 'file_exists', + 'is_writable', + 'is_writeable', + 'is_readable', + 'is_executable', + 'is_file', + 'is_dir', + 'is_link', + 'filectime', + 'fileatime', + 'filemtime', + 'fileinode', + 'filegroup', + 'fileowner', + 'filesize', + 'filetype', + 'fileperms', + 'ftell', + 'ini_get', + 'function_exists', + 'json_last_error', + 'json_last_error_msg', + ], true)) { + $this->functions[] = $functionName; break 2; } + + // PhpStorm stub's #[Pure(true)] means the function has side effects but its return value is important. + // In PHPStan's criteria, these functions are simply considered as ['hasSideEffect' => true]. + if (isset($attr->args[0]->value->name->name) && $attr->args[0]->value->name->name === 'true') { + $this->impureFunctions[] = $functionName; + } else { + $this->functions[] = $functionName; + } + break 2; } } } @@ -74,26 +119,37 @@ public function enterNode(Node $node) ); } + /** @var array $metadata */ $metadata = require __DIR__ . '/functionMetadata_original.php'; foreach ($visitor->functions as $functionName) { if (array_key_exists($functionName, $metadata)) { if ($metadata[$functionName]['hasSideEffects']) { - if (in_array($functionName, [ - 'mt_rand', - 'rand', - 'random_bytes', - 'random_int', - 'connection_aborted', - 'connection_status', - 'file_get_contents', - ], true)) { - continue; - } 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)) { diff --git a/bin/generate-rule-error-classes.php b/bin/generate-rule-error-classes.php index b8b3219e5d..6b8284669e 100755 --- a/bin/generate-rule-error-classes.php +++ b/bin/generate-rule-error-classes.php @@ -14,7 +14,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError%s implements %s +final class RuleError%s implements %s { %s @@ -47,7 +47,7 @@ class RuleError%s implements %s $typeCombination, implode(', ', $interfaces), implode("\n\n\t", array_map(static fn (array $property): string => sprintf('%spublic %s $%s;', $property[2] !== $property[1] ? sprintf("/** @var %s */\n\t", $property[2]) : '', $property[1], $property[0]), $properties)), - implode("\n\n\t", array_map(static fn (array $property): string => sprintf("%spublic function get%s(): %s\n\t{\n\t\treturn \$this->%s;\n\t}", $property[2] !== $property[1] ? sprintf("/**\n\t * @return %s\n\t */\n\t", $property[2]) : '', ucfirst($property[0]), $property[1], $property[0]), $properties)), + 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 bb97758ff6..23e5725d38 100755 --- a/bin/phpstan +++ b/bin/phpstan @@ -11,17 +11,8 @@ 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'); - } - - if (PHP_VERSION_ID < 70300) { - gc_disable(); // performance boost - } define('__PHPSTAN_RUNNING__', true); @@ -38,50 +29,6 @@ use Symfony\Component\Console\Helper\ProgressBar; } $devOrPharLoader->unregister(); - $composerAutoloadFiles = $GLOBALS['__composer_autoload_files']; - if ( - !array_key_exists('e88992873b7765f9b5710cab95ba5dd7', $composerAutoloadFiles) - || !array_key_exists('3e76f7f02b41af8cea96018933f6b7e3', $composerAutoloadFiles) - || !array_key_exists('a4a119a56e50fbb293281d9a48007e0e', $composerAutoloadFiles) - || !array_key_exists('0e6d7bf4a5811bfa5cf40c5ccd6fae6a', $composerAutoloadFiles) - || !array_key_exists('e69f7f6ee287b969198c3c9d6777bd38', $composerAutoloadFiles) - || !array_key_exists('0d59ee240a4cd96ddbb4ff164fccea4d', $composerAutoloadFiles) - || !array_key_exists('b686b8e46447868025a15ce5d0cb2634', $composerAutoloadFiles) - || !array_key_exists('8825ede83f2f289127722d4e842cf7e8', $composerAutoloadFiles) - || !array_key_exists('23c18046f52bef3eea034657bafda50f', $composerAutoloadFiles) - ) { - echo "Composer autoloader changed\n"; - exit(1); - } - - // 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, - - // vendor/symfony/polyfill-php80/bootstrap.php - 'a4a119a56e50fbb293281d9a48007e0e' => true, - - // vendor/symfony/polyfill-mbstring/bootstrap.php - '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => true, - - // vendor/symfony/polyfill-intl-normalizer/bootstrap.php - 'e69f7f6ee287b969198c3c9d6777bd38' => true, - - // vendor/symfony/polyfill-php73/bootstrap.php - '0d59ee240a4cd96ddbb4ff164fccea4d' => true, - - // vendor/symfony/polyfill-php74/bootstrap.php - 'b686b8e46447868025a15ce5d0cb2634' => true, - - // vendor/symfony/polyfill-intl-grapheme/bootstrap.php - '8825ede83f2f289127722d4e842cf7e8' => true, - - // vendor/symfony/polyfill-php81/bootstrap.php - '23c18046f52bef3eea034657bafda50f' => true, - ]; - $autoloaderInWorkingDirectory = $vendorDirectory . '/autoload.php'; $composerAutoloaderProjectPaths = []; @@ -159,7 +106,11 @@ use Symfony\Component\Console\Helper\ProgressBar; $application->setDefaultCommand('analyse'); ProgressBar::setFormatDefinition('file_download', ' [%bar%] %percent:3s%% %fileSize%'); + $composerAutoloaderProjectPaths = array_map(function(string $s): string { + return str_replace(DIRECTORY_SEPARATOR, '/', $s); + }, $composerAutoloaderProjectPaths); $reversedComposerAutoloaderProjectPaths = array_values(array_unique(array_reverse($composerAutoloaderProjectPaths))); + $application->add(new AnalyseCommand($reversedComposerAutoloaderProjectPaths, $analysisStartTime)); $application->add(new WorkerCommand($reversedComposerAutoloaderProjectPaths)); $application->add(new ClearResultCacheCommand($reversedComposerAutoloaderProjectPaths)); 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/src/Internal/ContainerDynamicReturnTypeExtension.php b/build/PHPStan/Build/ContainerDynamicReturnTypeExtension.php similarity index 75% rename from src/Internal/ContainerDynamicReturnTypeExtension.php rename to build/PHPStan/Build/ContainerDynamicReturnTypeExtension.php index f4b0a340bb..8e43bd2d47 100644 --- a/src/Internal/ContainerDynamicReturnTypeExtension.php +++ b/build/PHPStan/Build/ContainerDynamicReturnTypeExtension.php @@ -1,6 +1,6 @@ getArgs()) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $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()); 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 51874b7a23..22812240f4 100644 --- a/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php +++ b/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php @@ -9,7 +9,7 @@ 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 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-7.4.neon b/build/baseline-7.4.neon index 82e6b89e0c..b28b9f9f5d 100644 --- a/build/baseline-7.4.neon +++ b/build/baseline-7.4.neon @@ -77,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 3e0d07184d..c7539013c2 100644 --- a/build/baseline-8.0.neon +++ b/build/baseline-8.0.neon @@ -1,7 +1,7 @@ parameters: ignoreErrors: - - message: "#^Strict comparison using \\=\\=\\= between array and false will always evaluate to false\\.$#" + message: "#^Strict comparison using \\=\\=\\= between list and false will always evaluate to false\\.$#" count: 1 path: ../src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php @@ -24,13 +24,3 @@ parameters: message: "#^Strict comparison using \\=\\=\\= between list\\ and false will always evaluate to false\\.$#" count: 1 path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php - - - - message: "#^Strict comparison using \\=\\=\\= between list and false will always evaluate to false\\.$#" - count: 1 - path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php - - - - message: "#^Call to function is_bool\\(\\) with string will always evaluate to false\\.$#" - count: 1 - path: ../src/Type/Php/SubstrDynamicReturnTypeExtension.php diff --git a/build/baseline-lt-7.3.neon b/build/baseline-lt-7.3.neon deleted file mode 100644 index aab4991158..0000000000 --- a/build/baseline-lt-7.3.neon +++ /dev/null @@ -1,2 +0,0 @@ -parameters: - ignoreErrors: [] diff --git a/build/collision-detector.json b/build/collision-detector.json index 21228704f5..a687cd3ea4 100644 --- a/build/collision-detector.json +++ b/build/collision-detector.json @@ -5,12 +5,16 @@ "../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/Levels/data/stubs/function.php", + "../tests/PHPStan/Rules/Properties/data/abstract-final-property-hook-parse-error.php", + "../tests/PHPStan/Rules/Properties/data/final-property-hooks.php" + ] } diff --git a/build/composer-dependency-analyser.php b/build/composer-dependency-analyser.php index de637bfc79..79dc694253 100644 --- a/build/composer-dependency-analyser.php +++ b/build/composer-dependency-analyser.php @@ -9,8 +9,6 @@ 'symfony/polyfill-intl-grapheme', 'symfony/polyfill-intl-normalizer', 'symfony/polyfill-mbstring', - 'symfony/polyfill-php73', - 'symfony/polyfill-php74', 'symfony/polyfill-php80', 'symfony/polyfill-php81', ]; @@ -19,6 +17,7 @@ 'symfony/process', 'symfony/service-contracts', 'symfony/string', + 'nette/php-generator', ]; return $config @@ -32,7 +31,7 @@ ) ->ignoreErrorsOnPackage('phpunit/phpunit', [ErrorType::DEV_DEPENDENCY_IN_PROD]) // prepared test tooling ->ignoreErrorsOnPackage('jetbrains/phpstorm-stubs', [ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV]) // there is no direct usage, but we need newer version then required by ondrejmirtes/BetterReflection - ->ignoreErrorsOnPath(__DIR__ . '/../tests', [ErrorType::UNKNOWN_CLASS, ErrorType::UNKNOWN_FUNCTION]) // to be able to test invalid symbols + ->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/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 index a440588e85..437bfcf1cc 100644 --- a/build/downgrade.php +++ b/build/downgrade.php @@ -1,7 +1,9 @@ __DIR__ . '/../composer.json', 'paths' => [ + __DIR__ . '/../build/PHPStan', __DIR__ . '/../src', __DIR__ . '/../tests/PHPStan', __DIR__ . '/../tests/e2e', diff --git a/build/enum-adapter-errors.neon b/build/enum-adapter-errors.neon deleted file mode 100644 index 04a18bb846..0000000000 --- a/build/enum-adapter-errors.neon +++ /dev/null @@ -1,151 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^Call to method getAttributes\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 2 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method getBackingType\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method getValueExpression\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnumBackedCase\\.$#" - count: 2 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method getCase\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method getCases\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method getConstructor\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method getDocComment\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method getFileName\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method getName\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method getParentClass\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 4 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method getTraits\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method hasCase\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method implementsInterface\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method isAbstract\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method isBacked\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 2 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method isEnum\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method isFinal\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method isInterface\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 2 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method isInternal\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method isReadOnly\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method isSubclassOf\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method isTrait\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum not found\\.$#" - count: 4 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnumBackedCase not found\\.$#" - count: 2 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Method PHPStan\\\\Reflection\\\\ClassReflection\\:\\:getNativeReflection\\(\\) has invalid return type PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Parameter \\$class of method PHPStan\\\\Reflection\\\\ClassReflection\\:\\:collectTraits\\(\\) has invalid type PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Parameter \\$reflection of method PHPStan\\\\Reflection\\\\ClassReflection\\:\\:__construct\\(\\) has invalid type PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Property PHPStan\\\\Reflection\\\\ClassReflection\\:\\:\\$reflection has unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum as its type\\.$#" - count: 1 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\BuiltinMethodReflection\\:\\:getDeclaringClass\\(\\) has invalid return type PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/Php/BuiltinMethodReflection.php - - - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\NativeBuiltinMethodReflection\\:\\:getDeclaringClass\\(\\) has invalid return type PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/Php/NativeBuiltinMethodReflection.php diff --git a/build/enums.neon b/build/enums.neon index 3ec87ab42e..44eaccbbd1 100644 --- a/build/enums.neon +++ b/build/enums.neon @@ -13,3 +13,7 @@ parameters: 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/ignore-by-architecture.neon.php b/build/ignore-by-architecture.neon.php index a6c86b46cf..4b2208f5fe 100644 --- a/build/ignore-by-architecture.neon.php +++ b/build/ignore-by-architecture.neon.php @@ -1,11 +1,12 @@ load(__DIR__ . '/baseline-32bit.neon'); + $config = []; + $config['includes'] = [ + __DIR__ . '/baseline-32bit.neon', + ]; + + return $config; } return []; diff --git a/build/ignore-by-php-version.neon.php b/build/ignore-by-php-version.neon.php index 62de0070e5..8aeff82810 100644 --- a/build/ignore-by-php-version.neon.php +++ b/build/ignore-by-php-version.neon.php @@ -1,11 +1,6 @@ = 80000) { $includes[] = __DIR__ . '/baseline-8.0.neon'; } @@ -20,10 +15,6 @@ $includes[] = __DIR__ . '/ignore-gte-php7.4-errors.neon'; } -if (PHP_VERSION_ID < 70400) { - $includes[] = __DIR__ . '/enum-adapter-errors.neon'; -} - if (PHP_VERSION_ID < 80000) { $includes[] = __DIR__ . '/more-enum-adapter-errors.neon'; } @@ -38,6 +29,15 @@ $includes[] = __DIR__ . '/datetime-php-83.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; diff --git a/build/ignore-gte-php7.4-errors.neon b/build/ignore-gte-php7.4-errors.neon index d5ae8bada3..112fb17e4c 100644 --- a/build/ignore-gte-php7.4-errors.neon +++ b/build/ignore-gte-php7.4-errors.neon @@ -3,7 +3,7 @@ includes: parameters: ignoreErrors: - - '#^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#' diff --git a/build/more-enum-adapter-errors.neon b/build/more-enum-adapter-errors.neon index 69882bcfc5..4e7e2ee083 100644 --- a/build/more-enum-adapter-errors.neon +++ b/build/more-enum-adapter-errors.neon @@ -1,9 +1,5 @@ parameters: ignoreErrors: - - - message: "#^Parameter \\#1 \\$expected of method PHPUnit\\\\Framework\\\\Assert\\:\\:assertInstanceOf\\(\\) expects class\\-string\\, string given\\.$#" - count: 1 - path: ../tests/PHPStan/Reflection/ClassReflectionTest.php - message: "#^Strict comparison using \\!\\=\\= between class\\-string and 'UnitEnum' will always evaluate to true\\.$#" count: 1 @@ -23,3 +19,6 @@ parameters: message: "#^Class BackedEnum not found\\.$#" count: 1 path: ../src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php + + - + message: "#^Call to method PHPStan\\\\Reflection\\\\ClassReflection::isEnum\\(\\) will always evaluate to false\\.$#" diff --git a/build/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..634de52560 --- /dev/null +++ b/build/old-phpunit.neon @@ -0,0 +1,34 @@ +parameters: + 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 bd0a5c6422..1e79bf303a 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -4,11 +4,13 @@ includes: - ../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: @@ -24,6 +26,7 @@ parameters: excludePaths: - ../tests/*/data/* - ../tests/tmp/* + - ../tests/vendor/* - ../tests/PHPStan/Analyser/nsrt/* - ../tests/PHPStan/Analyser/traits/* - ../tests/notAutoloaded/* @@ -56,11 +59,11 @@ 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' - 'LogicException' - 'Error' check: @@ -72,10 +75,33 @@ 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 T of PHPStan\\Rules\\RuleError\.$#' @@ -88,7 +114,6 @@ parameters: message: "#^Parameter \\#1 (?:\\$argument|\\$objectOrClass) of class ReflectionClass constructor expects class\\-string\\\\|PHPStan\\\\ExtensionInstaller\\\\GeneratedConfig, string given\\.$#" count: 1 path: ../src/Diagnose/PHPStanDiagnoseExtension.php - - '#^Parameter \#1 \$offsetType of class PHPStan\\Type\\Accessory\\HasOffsetType constructor expects PHPStan\\Type\\Constant\\ConstantIntegerType\|PHPStan\\Type\\Constant\\ConstantStringType#' - '#^Short ternary operator is not allowed#' reportStaticMethodSignatures: true tmpDir: %rootDir%/tmp @@ -97,6 +122,16 @@ parameters: - 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: - @@ -104,6 +139,6 @@ services: tags: - phpstan.broker.dynamicMethodReturnTypeExtension - - class: PHPStan\Internal\ContainerDynamicReturnTypeExtension + class: PHPStan\Build\ContainerDynamicReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension diff --git a/build/spl-autoload-functions-php-8.neon b/build/spl-autoload-functions-php-8.neon index b534d6c94f..3669321889 100644 --- a/build/spl-autoload-functions-php-8.neon +++ b/build/spl-autoload-functions-php-8.neon @@ -1,6 +1,6 @@ parameters: ignoreErrors: - - message: "#^PHPDoc tag @var with type array\\\\|false is not subtype of native type array\\.$#" + 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 index abafcfbf08..42cd820e71 100644 --- a/build/spl-autoload-functions-pre-php-7.neon +++ b/build/spl-autoload-functions-pre-php-7.neon @@ -1,10 +1,5 @@ parameters: ignoreErrors: - - - message: "#^PHPDoc tag @var with type array\\\\|false is not subtype of native type list\\\\|false\\.$#" - count: 2 - path: ../src/Command/CommandHelper.php - - message: '#^Parameter \#1 \$array \(list\) of array_values is already a list, call has no effect\.$#' path: ../src/Type/TypeCombinator.php diff --git a/build/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/changelog-generator/phpstan.neon b/changelog-generator/phpstan.neon index d187d34c96..a1fee9d3a7 100644 --- a/changelog-generator/phpstan.neon +++ b/changelog-generator/phpstan.neon @@ -11,4 +11,6 @@ parameters: paths: - src - run.php - checkGenericClassInNonGenericObjectType: false + ignoreErrors: + - + identifier: missingType.generics diff --git a/changelog-generator/run.php b/changelog-generator/run.php index fc709b521a..b163813296 100755 --- a/changelog-generator/run.php +++ b/changelog-generator/run.php @@ -118,6 +118,9 @@ protected function execute(InputInterface $input, OutputInterface $output) $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 { 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..f83b3a8b2d --- /dev/null +++ b/compiler/box/composer.lock @@ -0,0 +1,4085 @@ +{ + "_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.6", + "source": { + "type": "git", + "url": "https://github.com/box-project/box.git", + "reference": "09646041cb2e0963ab6ca109e1b366337617a0f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/box-project/box/zipball/09646041cb2e0963ab6ca109e1b366337617a0f2", + "reference": "09646041cb2e0963ab6ca109e1b366337617a0f2", + "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": "^5.2.12", + "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", + "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" + }, + "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.6" + }, + "time": "2025-03-02T18:20:45+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": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "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/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0" + }, + "time": "2024-07-06T21:00:26+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": "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 100755 index 402ceabdc4..0000000000 Binary files a/compiler/build/box.phar and /dev/null differ diff --git a/compiler/build/scoper.inc.php b/compiler/build/scoper.inc.php index 0ea6df31ec..be398d7de7 100644 --- a/compiler/build/scoper.inc.php +++ b/compiler/build/scoper.inc.php @@ -20,8 +20,6 @@ '../../vendor/symfony/polyfill-php81', '../../vendor/symfony/polyfill-mbstring', '../../vendor/symfony/polyfill-intl-normalizer', - '../../vendor/symfony/polyfill-php73', - '../../vendor/symfony/polyfill-php74', '../../vendor/symfony/polyfill-intl-grapheme', ]) as $file) { if ($file->getPathName() === '../../vendor/jetbrains/phpstorm-stubs/PhpStormStubsMap.php') { @@ -45,37 +43,21 @@ 'prefix' => $prefix, 'finders' => [], '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 !== 'bin/phpstan') { - return $content; - } - return str_replace(sprintf('%s\\\\__PHPSTAN_RUNNING__', $prefix), '__PHPSTAN_RUNNING__', $content); - }, function (string $filePath, string $prefix, string $content): string { if ($filePath !== 'vendor/nette/di/src/DI/Compiler.php') { return $content; } - 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/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 ); @@ -86,25 +68,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 (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; }, @@ -184,7 +161,7 @@ function (string $filePath, string $prefix, string $content): string { return $content; } - $content = str_replace('\'' . $prefix . '\\\\', '\'', $content); + $content = str_replace('\'' . $prefix . '\\', '\'', $content); return $content; }, @@ -193,7 +170,7 @@ function (string $filePath, string $prefix, string $content): string { return $content; } - $content = str_replace('\'' . $prefix . '\\\\', '\'', $content); + $content = str_replace('\'' . $prefix . '\\', '\'', $content); return $content; }, @@ -209,7 +186,7 @@ function (string $filePath, string $prefix, string $content): string { return $content; } - return str_replace(sprintf('\'%s\\\\JetBrains\\\\', $prefix), '\'JetBrains\\\\', $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')) { @@ -218,6 +195,47 @@ function (string $filePath, string $prefix, string $content): string { return str_replace(sprintf('use %s\\PhpParser;', $prefix), 'use PhpParser;', $content); }, + function (string $filePath, string $prefix, string $content): string { + if (!str_starts_with($filePath, 'vendor/nikic/php-parser/lib')) { + return $content; + } + + return str_replace([ + sprintf('\\%s', $prefix), + sprintf('\\\\%s', $prefix), + ], '', $content); + }, + function (string $filePath, string $prefix, string $content): string { + if (!str_starts_with($filePath, 'vendor/ondrejmirtes/better-reflection')) { + return $content; + } + + return str_replace(sprintf('%s\\PropertyHookType', $prefix), 'PropertyHookType', $content); + }, + function (string $filePath, string $prefix, string $content): string { + if (strpos($filePath, 'src/') !== 0) { + return $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); + }, ], 'exclude-namespaces' => [ 'PHPStan', @@ -228,8 +246,6 @@ function (string $filePath, string $prefix, string $content): string { 'Symfony\Polyfill\Php81', 'Symfony\Polyfill\Mbstring', 'Symfony\Polyfill\Intl\Normalizer', - 'Symfony\Polyfill\Php73', - 'Symfony\Polyfill\Php74', 'Symfony\Polyfill\Intl\Grapheme', ], 'expose-global-functions' => false, diff --git a/compiler/composer.json b/compiler/composer.json index 4da1b08076..5b2f1b0d5d 100644 --- a/compiler/composer.json +++ b/compiler/composer.json @@ -6,11 +6,12 @@ "require": { "php": "^8.0", "nette/neon": "^3.0.0", + "ondrejmirtes/simple-downgrader": "^2.2.1", "seld/phar-utils": "^1.2", - "symfony/console": "^6.0.0", - "symfony/filesystem": "^6.0.0", - "symfony/finder": "^6.0.0", - "symfony/process": "^6.0.0" + "symfony/console": "^5.4.43", + "symfony/filesystem": "^5.4.43", + "symfony/finder": "^5.4.43", + "symfony/process": "^5.4.43" }, "autoload": { "psr-4": { @@ -23,8 +24,9 @@ } }, "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": { diff --git a/compiler/composer.lock b/compiler/composer.lock index d970a7b6f7..0da5aa0691 100644 --- a/compiler/composer.lock +++ b/compiler/composer.lock @@ -4,25 +4,73 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7caab5611acadb20806eaeca4e294e98", + "content-hash": "ce39326567575644f63f099981a3cd73", "packages": [ + { + "name": "jetbrains/phpstorm-stubs", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/JetBrains/phpstorm-stubs.git", + "reference": "dee3c94edb5adde5834e42c3d660a2cc89d367ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/dee3c94edb5adde5834e42c3d660a2cc89d367ea", + "reference": "dee3c94edb5adde5834e42c3d660a2cc89d367ea", + "shasum": "" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^v3.64.0", + "nikic/php-parser": "^v5.3.1", + "phpdocumentor/reflection-docblock": "^5.6.0", + "phpunit/phpunit": "^11.4.3" + }, + "default-branch": true, + "type": "library", + "autoload": { + "files": [ + "PhpStormStubsMap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "PHP runtime & extensions header files for PhpStorm", + "homepage": "https://www.jetbrains.com/phpstorm", + "keywords": [ + "autocomplete", + "code", + "inference", + "inspection", + "jetbrains", + "phpstorm", + "stubs", + "type" + ], + "support": { + "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" + }, + "time": "2025-07-08T09:17:06+00:00" + }, { "name": "nette/neon", - "version": "v3.4.0", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/nette/neon.git", - "reference": "372d945c156ee7f35c953339fb164538339e6283" + "reference": "3411aa86b104e2d5b7e760da4600865ead963c3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/neon/zipball/372d945c156ee7f35c953339fb164538339e6283", - "reference": "372d945c156ee7f35c953339fb164538339e6283", + "url": "https://api.github.com/repos/nette/neon/zipball/3411aa86b104e2d5b7e760da4600865ead963c3c", + "reference": "3411aa86b104e2d5b7e760da4600865ead963c3c", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=8.0 <8.3" + "php": "8.0 - 8.4" }, "require-dev": { "nette/tester": "^2.4", @@ -70,9 +118,322 @@ ], "support": { "issues": "https://github.com/nette/neon/issues", - "source": "https://github.com/nette/neon/tree/v3.4.0" + "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": "2023-01-13T03:08:29+00:00" + "time": "2025-07-27T20:03:57+00:00" + }, + { + "name": "ondrejmirtes/better-reflection", + "version": "6.57.0.0", + "source": { + "type": "git", + "url": "https://github.com/ondrejmirtes/BetterReflection.git", + "reference": "dcc22b90a63497f3450dd5eed62197bc46937297" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/dcc22b90a63497f3450dd5eed62197bc46937297", + "reference": "dcc22b90a63497f3450dd5eed62197bc46937297", + "shasum": "" + }, + "require": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "dev-master#dfcad4524db603bd20bdec3aab1a31c5f5128ea3", + "nikic/php-parser": "^5.4.0", + "php": "^7.4 || ^8.0" + }, + "conflict": { + "thecodingmachine/safe": "<1.1.3" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0.0", + "phpstan/phpstan": "^1.10.60", + "phpstan/phpstan-phpunit": "^1.3.16", + "phpunit/phpunit": "^11.5.7", + "rector/rector": "1.2.10" + }, + "suggest": { + "composer/composer": "Required to use the ComposerSourceLocator" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\BetterReflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "James Titcumb", + "email": "james@asgrim.com", + "homepage": "https://github.com/asgrim" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + }, + { + "name": "Gary Hockin", + "email": "gary@roave.com", + "homepage": "https://github.com/geeh" + }, + { + "name": "Jaroslav Hanslík", + "email": "kukulich@kukulich.cz", + "homepage": "https://github.com/kukulich" + } + ], + "description": "Better Reflection - an improved code reflection API", + "support": { + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.57.0.0" + }, + "time": "2025-02-12T21:16:38+00:00" + }, + { + "name": "ondrejmirtes/simple-downgrader", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/ondrejmirtes/simple-downgrader.git", + "reference": "f91211786fcb75966ccba69406926db89cea8956" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ondrejmirtes/simple-downgrader/zipball/f91211786fcb75966ccba69406926db89cea8956", + "reference": "f91211786fcb75966ccba69406926db89cea8956", + "shasum": "" + }, + "require": { + "nette/utils": "^3.2.5", + "nikic/php-parser": "^5.5.0", + "ondrejmirtes/better-reflection": "^6.57", + "php": "^7.4|^8.0", + "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/" + ] + } + }, + "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.1" + }, + "time": "2025-07-13T11:49:36+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", @@ -177,42 +538,46 @@ }, { "name": "symfony/console", - "version": "v6.0.19", + "version": "v5.4.47", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c3ebc83d031b71c39da318ca8b7a07ecc67507ed" + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c3ebc83d031b71c39da318ca8b7a07ecc67507ed", - "reference": "c3ebc83d031b71c39da318ca8b7a07ecc67507ed", + "url": "https://api.github.com/repos/symfony/console/zipball/c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", "symfony/service-contracts": "^1.1|^2|^3", - "symfony/string": "^5.4|^6.0" + "symfony/string": "^5.1|^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" + "psr/log": ">=3", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" }, "provide": { - "psr/log-implementation": "1.0|2.0|3.0" + "psr/log-implementation": "1.0|2.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" + "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", @@ -247,12 +612,12 @@ "homepage": "https://symfony.com", "keywords": [ "cli", - "command line", + "command-line", "console", "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.0.19" + "source": "https://github.com/symfony/console/tree/v5.4.47" }, "funding": [ { @@ -268,26 +633,97 @@ "type": "tidelift" } ], - "time": "2023-01-01T08:36:10+00:00" + "time": "2024-11-06T11:30:55+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/26954b3d62a6c5fd0ea8a2a00c0353a14978d05c", + "reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c", + "shasum": "" + }, + "require": { + "php": ">=8.0.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.0-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.0.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:55:41+00:00" }, { "name": "symfony/filesystem", - "version": "v6.0.19", + "version": "v5.4.45", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "3d49eec03fda1f0fc19b7349fbbe55ebc1004214" + "reference": "57c8294ed37d4a055b77057827c67f9558c95c54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/3d49eec03fda1f0fc19b7349fbbe55ebc1004214", - "reference": "3d49eec03fda1f0fc19b7349fbbe55ebc1004214", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/57c8294ed37d4a055b77057827c67f9558c95c54", + "reference": "57c8294ed37d4a055b77057827c67f9558c95c54", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=7.2.5", "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8" + "symfony/polyfill-mbstring": "~1.8", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "symfony/process": "^5.4|^6.4" }, "type": "library", "autoload": { @@ -315,7 +751,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.0.19" + "source": "https://github.com/symfony/filesystem/tree/v5.4.45" }, "funding": [ { @@ -331,24 +767,26 @@ "type": "tidelift" } ], - "time": "2023-01-20T17:44:14+00:00" + "time": "2024-10-22T13:05:35+00:00" }, { "name": "symfony/finder", - "version": "v6.0.19", + "version": "v5.4.45", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "5cc9cac6586fc0c28cd173780ca696e419fefa11" + "reference": "63741784cd7b9967975eec610b256eed3ede022b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/5cc9cac6586fc0c28cd173780ca696e419fefa11", - "reference": "5cc9cac6586fc0c28cd173780ca696e419fefa11", + "url": "https://api.github.com/repos/symfony/finder/zipball/63741784cd7b9967975eec610b256eed3ede022b", + "reference": "63741784cd7b9967975eec610b256eed3ede022b", "shasum": "" }, "require": { - "php": ">=8.0.2" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php80": "^1.16" }, "type": "library", "autoload": { @@ -376,7 +814,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.0.19" + "source": "https://github.com/symfony/finder/tree/v5.4.45" }, "funding": [ { @@ -392,24 +830,24 @@ "type": "tidelift" } ], - "time": "2023-01-20T17:44:14+00:00" + "time": "2024-09-28T13:32:08+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.27.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -419,12 +857,9 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -458,7 +893,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -474,36 +909,33 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.27.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/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.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -539,7 +971,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -555,36 +987,33 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.27.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/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.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -623,7 +1052,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -639,24 +1068,25 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-iconv": "*", + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -666,12 +1096,9 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -706,7 +1133,163 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + "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-php73", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "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\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.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-php80", + "version": "v1.32.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.32.0" }, "funding": [ { @@ -722,24 +1305,25 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { "name": "symfony/process", - "version": "v6.0.19", + "version": "v5.4.47", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "2114fd60f26a296cc403a7939ab91478475a33d4" + "reference": "5d1662fb32ebc94f17ddb8d635454a776066733d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/2114fd60f26a296cc403a7939ab91478475a33d4", - "reference": "2114fd60f26a296cc403a7939ab91478475a33d4", + "url": "https://api.github.com/repos/symfony/process/zipball/5d1662fb32ebc94f17ddb8d635454a776066733d", + "reference": "5d1662fb32ebc94f17ddb8d635454a776066733d", "shasum": "" }, "require": { - "php": ">=8.0.2" + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" }, "type": "library", "autoload": { @@ -767,7 +1351,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.0.19" + "source": "https://github.com/symfony/process/tree/v5.4.47" }, "funding": [ { @@ -783,7 +1367,7 @@ "type": "tidelift" } ], - "time": "2023-01-01T08:36:10+00:00" + "time": "2024-11-06T11:36:42+00:00" }, { "name": "symfony/service-contracts", @@ -811,12 +1395,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.0-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -1026,16 +1610,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.13.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", "shasum": "" }, "require": { @@ -1043,11 +1627,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "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", @@ -1073,7 +1658,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" }, "funding": [ { @@ -1081,80 +1666,25 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" - }, - { - "name": "nikic/php-parser", - "version": "v4.17.1", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.0" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.9-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" - }, - "time": "2023-08-13T19:53:39+00:00" + "time": "2025-04-29T12:36:36+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "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", @@ -1195,9 +1725,15 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -1252,20 +1788,20 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.15", + "version": "2.1.21", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "762c4dac4da6f8756eebb80e528c3a47855da9bd" + "reference": "1ccf445757458c06a04eb3f803603cb118fe5fa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/762c4dac4da6f8756eebb80e528c3a47855da9bd", - "reference": "762c4dac4da6f8756eebb80e528c3a47855da9bd", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1ccf445757458c06a04eb3f803603cb118fe5fa6", + "reference": "1ccf445757458c06a04eb3f803603cb118fe5fa6", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -1304,40 +1840,37 @@ { "url": "https://github.com/phpstan", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" } ], - "time": "2023-05-09T15:28:01+00:00" + "time": "2025-07-28T19:35:08+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.3.13", + "version": "2.0.7", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "d8bdab0218c5eb0964338d24a8511b65e9c94fa5" + "reference": "9a9b161baee88a5f5c58d816943cff354ff233dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/d8bdab0218c5eb0964338d24a8511b65e9c94fa5", - "reference": "d8bdab0218c5eb0964338d24a8511b65e9c94fa5", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/9a9b161baee88a5f5c58d816943cff354ff233dc", + "reference": "9a9b161baee88a5f5c58d816943cff354ff233dc", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10" + "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" }, "type": "phpstan-extension", "extra": { @@ -1360,41 +1893,41 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.13" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.7" }, - "time": "2023-05-26T11:05:59+00:00" + "time": "2025-07-13T11:31:46+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.27", + "version": "9.2.32", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1" + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b0a88255cb70d52653d80c890bd7f38740ea50d1", - "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1", + "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.15", + "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": "PHP extension that provides line coverage", @@ -1403,7 +1936,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -1432,7 +1965,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.27" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { @@ -1440,7 +1973,7 @@ "type": "github" } ], - "time": "2023-07-26T13:44:30+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1685,45 +2218,45 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.11", + "version": "9.6.23", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "810500e92855eba8a7a5319ae913be2da6f957b0" + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/810500e92855eba8a7a5319ae913be2da6f957b0", - "reference": "810500e92855eba8a7a5319ae913be2da6f957b0", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1 || ^2", + "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.3", - "phar-io/version": "^3.0.2", + "myclabs/deep-copy": "^1.13.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.13", - "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", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.5", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.2", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.6", + "sebastian/global-state": "^5.0.7", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", "sebastian/version": "^3.0.2" }, "suggest": { @@ -1768,7 +2301,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.11" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.23" }, "funding": [ { @@ -1779,25 +2312,33 @@ "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": "2023-08-19T07:10:56+00:00" + "time": "2025-05-02T06:40:34+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { @@ -1832,7 +2373,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": [ { @@ -1840,7 +2381,7 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-03-02T06:27:43+00:00" }, { "name": "sebastian/code-unit", @@ -2029,20 +2570,20 @@ }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -2074,7 +2615,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": [ { @@ -2082,20 +2623,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.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { @@ -2140,7 +2681,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" }, "funding": [ { @@ -2148,7 +2689,7 @@ "type": "github" } ], - "time": "2023-05-07T05:35:17+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { "name": "sebastian/environment", @@ -2215,16 +2756,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", "shasum": "" }, "require": { @@ -2280,7 +2821,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" }, "funding": [ { @@ -2288,20 +2829,20 @@ "type": "github" } ], - "time": "2022-09-14T06:03:37+00:00" + "time": "2024-03-02T06:33:00+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.6", + "version": "5.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bde739e7565280bda77be70044ac1047bc007e34" + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", - "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", "shasum": "" }, "require": { @@ -2344,7 +2885,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" }, "funding": [ { @@ -2352,24 +2893,24 @@ "type": "github" } ], - "time": "2023-08-02T09:26:13+00:00" + "time": "2024-03-02T06:35:11+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -2401,7 +2942,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": [ { @@ -2409,7 +2950,7 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", @@ -2588,16 +3129,16 @@ }, { "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": { @@ -2609,7 +3150,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -2630,8 +3171,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": [ { @@ -2639,7 +3179,7 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-03-14T16:00:52+00:00" }, { "name": "sebastian/type", @@ -2752,16 +3292,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.1", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -2790,7 +3330,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/1.2.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -2798,20 +3338,20 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+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": "^8.0" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.0.99" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/compiler/src/Console/PrepareCommand.php b/compiler/src/Console/PrepareCommand.php index f7f2b6cdf6..cc8ddbac5d 100644 --- a/compiler/src/Console/PrepareCommand.php +++ b/compiler/src/Console/PrepareCommand.php @@ -16,6 +16,7 @@ use function file_get_contents; use function file_put_contents; use function implode; +use function in_array; use function is_dir; use function json_decode; use function json_encode; @@ -62,9 +63,9 @@ private function fixComposerJson(string $buildDir): void { $json = json_decode($this->filesystem->read($buildDir . '/composer.json'), true); - unset($json['replace']); + unset($json['replace']['phpstan/phpstan']); $json['name'] = 'phpstan/phpstan'; - $json['require']['php'] = '^7.2|^8.0'; + $json['require']['php'] = '^7.4|^8.0'; // simplify autoload (remove not packed build directory] $json['autoload']['psr-4']['PHPStan\\'] = 'src/'; @@ -184,6 +185,20 @@ private function buildPreloadScript(): void 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"; } @@ -205,7 +220,7 @@ private function deleteUnnecessaryVendorCode(): void private function transformSource(): void { chdir(__DIR__ . '/../../..'); - exec(escapeshellarg(__DIR__ . '/../../../vendor/bin/simple-downgrade') . ' downgrade -c ' . escapeshellarg('build/downgrade.php') . ' 7.2', $outputLines, $exitCode); + exec(escapeshellarg(__DIR__ . '/../../vendor/bin/simple-downgrade') . ' downgrade -c ' . escapeshellarg('build/downgrade.php') . ' 7.4', $outputLines, $exitCode); if ($exitCode === 0) { return; } diff --git a/composer.json b/composer.json index 58a4c837ce..476ef2fd69 100644 --- a/composer.json +++ b/composer.json @@ -5,43 +5,45 @@ "MIT" ], "require": { - "php": "^8.1", + "php": "^8.2", "composer-runtime-api": "^2.0", "clue/ndjson-react": "^1.0", "composer/ca-bundle": "^1.2", "composer/semver": "^3.4", "composer/xdebug-handler": "^3.0.3", - "fidry/cpu-core-counter": "^0.5.0", + "fidry/cpu-core-counter": "^1.2", "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", - "jetbrains/phpstorm-stubs": "dev-master#bdc8acd7c04c0c87197849c7cdd27e44b67b56c7", + "jetbrains/phpstorm-stubs": "dev-master#6c034ae018ed7ac2cacf2474f824aede73658afa", "nette/bootstrap": "^3.0", "nette/di": "^3.1.4", - "nette/neon": "^3.3.1", + "nette/neon": "3.3.4", + "nette/php-generator": "3.6.9", "nette/schema": "^1.2.2", "nette/utils": "^3.2.5", - "nikic/php-parser": "^4.17.1", + "nikic/php-parser": "^5.6.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.25.0.13", - "phpstan/php-8-stubs": "0.3.95", - "phpstan/phpdoc-parser": "1.29.1", + "ondrejmirtes/better-reflection": "6.57.0.0", + "ondrejmirtes/composer-attribute-collector": "^1.1.1", + "ondrejmirtes/php-merge": "^4.1", + "phpstan/php-8-stubs": "0.4.17", + "phpstan/phpdoc-parser": "2.2.0", "psr/http-message": "^1.1", "react/async": "^3", - "react/child-process": "^0.6.4", + "react/child-process": "^0.7", "react/dns": "^1.10", "react/event-loop": "^1.2", "react/http": "^1.1", "react/promise": "^3.2", "react/socket": "^1.3", "react/stream": "^1.1", + "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-php73": "^1.23", - "symfony/polyfill-php74": "^1.23", "symfony/polyfill-php80": "^1.23", "symfony/polyfill-php81": "^1.27", "symfony/process": "^5.4.3", @@ -49,46 +51,61 @@ "symfony/string": "^5.4.3" }, "replace": { - "phpstan/phpstan": "self.version" + "phpstan/phpstan": "2.1.x", + "symfony/polyfill-php73": "*" }, "require-dev": { - "brianium/paratest": "^6.5", "cweagans/composer-patches": "^1.7.3", - "nette/finder": "^2.5", - "ondrejmirtes/simple-downgrader": "^1.0", "php-parallel-lint/php-parallel-lint": "^1.2.0", - "phpstan/phpstan-deprecation-rules": "^1.2", - "phpstan/phpstan-nette": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.6", - "phpunit/phpunit": "^9.5.4", + "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": "8.1.99" + "php": "8.2.99" }, "platform-check": false, "sort-packages": true, "allow-plugins": { - "cweagans/composer-patches": true + "cweagans/composer-patches": true, + "ondrejmirtes/composer-attribute-collector": true, + "vaimo/composer-patches": true } }, "extra": { + "composer-attribute-collector": { + "include": [ + "src" + ] + }, "composer-exit-on-patch-failure": true, "patches": { "composer/ca-bundle": [ "patches/cloudflare-ca.patch" ], + "hoa/exception": [ + "patches/Idle.patch" + ], + "hoa/file": [ + "patches/File.patch", + "patches/Read.patch" + ], "hoa/iterator": [ "patches/Buffer.patch", "patches/Lookahead.patch" ], "hoa/compiler": [ "patches/HoaException.patch", + "patches/Invocation.patch", "patches/Rule.patch", - "patches/Lexer.patch" + "patches/Lexer.patch", + "patches/TreeNode.patch" ], "hoa/consistency": [ "patches/Consistency.patch" @@ -109,6 +126,12 @@ ], "nette/di": [ "patches/DependencyChecker.patch" + ], + "react/http": [ + "patches/Sender.patch" + ], + "symfony/console": [ + "patches/OutputFormatter.patch" ] } }, @@ -118,7 +141,7 @@ "src/" ] }, - "files": ["src/dumpType.php", "src/autoloadFunctions.php", "src/Testing/functions.php"] + "files": ["src/debugScope.php", "src/dumpType.php", "src/autoloadFunctions.php", "src/Testing/functions.php"] }, "autoload-dev": { "psr-4": { diff --git a/composer.lock b/composer.lock index cbed841d93..339777589c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "db22a3caebfecfcc88bd82e014d25b19", + "content-hash": "06fe848fbbd59979793fe364dd0190dc", "packages": [ { "name": "clue/ndjson-react", @@ -72,16 +72,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.5.0", + "version": "1.5.7", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99" + "reference": "d665d22c417056996c59019579f1967dfe5c1e82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", - "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/d665d22c417056996c59019579f1967dfe5c1e82", + "reference": "d665d22c417056996c59019579f1967dfe5c1e82", "shasum": "" }, "require": { @@ -91,8 +91,8 @@ }, "require-dev": { "phpstan/phpstan": "^1.10", - "psr/log": "^1.0", - "symfony/phpunit-bridge": "^4.2 || ^5", + "phpunit/phpunit": "^8 || ^9", + "psr/log": "^1.0 || ^2.0 || ^3.0", "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "type": "library", @@ -128,7 +128,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.5.0" + "source": "https://github.com/composer/ca-bundle/tree/1.5.7" }, "funding": [ { @@ -144,34 +144,42 @@ "type": "tidelift" } ], - "time": "2024-03-15T14:00:32+00:00" + "time": "2025-05-26T15:08:54+00:00" }, { "name": "composer/pcre", - "version": "3.1.3", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", - "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, "require-dev": { - "phpstan/phpstan": "^1.3", + "phpstan/phpstan": "^1.11.10", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^5" + "phpunit/phpunit": "^8 || ^9" }, "type": "library", "extra": { "branch-alias": { "dev-main": "3.x-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] } }, "autoload": { @@ -199,7 +207,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.3" + "source": "https://github.com/composer/pcre/tree/3.3.1" }, "funding": [ { @@ -215,28 +223,28 @@ "type": "tidelift" } ], - "time": "2024-03-19T10:26:25+00:00" + "time": "2024-08-27T18:44:43+00:00" }, { "name": "composer/semver", - "version": "3.4.0", + "version": "3.4.3", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "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.4", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { @@ -280,7 +288,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.0" + "source": "https://github.com/composer/semver/tree/3.4.3" }, "funding": [ { @@ -296,7 +304,7 @@ "type": "tidelift" } ], - "time": "2023-08-31T09:50:34+00:00" + "time": "2024-09-19T14:15:21+00:00" }, { "name": "composer/xdebug-handler", @@ -413,16 +421,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "0.5.1", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "b58e5a3933e541dc286cc91fc4f3898bbc6f1623" + "reference": "8520451a140d3f46ac33042715115e290cf5785f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/b58e5a3933e541dc286cc91fc4f3898bbc6f1623", - "reference": "b58e5a3933e541dc286cc91fc4f3898bbc6f1623", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", "shasum": "" }, "require": { @@ -430,13 +438,13 @@ }, "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": "^9.5.26 || ^8.5.31", - "theofidry/php-cs-fixer-config": "^1.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", "webmozarts/strict-phpunit": "^7.5" }, "type": "library", @@ -462,7 +470,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/0.5.1" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" }, "funding": [ { @@ -470,7 +478,7 @@ "type": "github" } ], - "time": "2022-12-24T12:35:10+00:00" + "time": "2024-08-06T10:04:20+00:00" }, { "name": "fig/http-message-util", @@ -1434,19 +1442,19 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "bdc8acd7c04c0c87197849c7cdd27e44b67b56c7" + "reference": "6c034ae018ed7ac2cacf2474f824aede73658afa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/bdc8acd7c04c0c87197849c7cdd27e44b67b56c7", - "reference": "bdc8acd7c04c0c87197849c7cdd27e44b67b56c7", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/6c034ae018ed7ac2cacf2474f824aede73658afa", + "reference": "6c034ae018ed7ac2cacf2474f824aede73658afa", "shasum": "" }, "require-dev": { - "friendsofphp/php-cs-fixer": "v3.46.0", - "nikic/php-parser": "v5.0.0", - "phpdocumentor/reflection-docblock": "5.3.0", - "phpunit/phpunit": "10.5.5" + "friendsofphp/php-cs-fixer": "^v3.64.0", + "nikic/php-parser": "^v5.3.1", + "phpdocumentor/reflection-docblock": "^5.6.0", + "phpunit/phpunit": "^11.4.3" }, "default-branch": true, "type": "library", @@ -1474,7 +1482,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2024-07-05T11:52:49+00:00" + "time": "2025-07-23T09:41:14+00:00" }, { "name": "nette/bootstrap", @@ -1698,21 +1706,21 @@ }, { "name": "nette/neon", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/nette/neon.git", - "reference": "54b287d8c2cdbe577b02e28ca1713e275b05ece2" + "reference": "bb88bf3a54dd21bf4dbddb5cd525d7b0c61b7cda" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/neon/zipball/54b287d8c2cdbe577b02e28ca1713e275b05ece2", - "reference": "54b287d8c2cdbe577b02e28ca1713e275b05ece2", + "url": "https://api.github.com/repos/nette/neon/zipball/bb88bf3a54dd21bf4dbddb5cd525d7b0c61b7cda", + "reference": "bb88bf3a54dd21bf4dbddb5cd525d7b0c61b7cda", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=7.1" + "php": "7.1 - 8.4" }, "require-dev": { "nette/tester": "^2.0", @@ -1760,27 +1768,27 @@ ], "support": { "issues": "https://github.com/nette/neon/issues", - "source": "https://github.com/nette/neon/tree/v3.3.2" + "source": "https://github.com/nette/neon/tree/v3.3.4" }, - "time": "2021-11-25T15:57:41+00:00" + "time": "2024-10-04T22:17:24+00:00" }, { "name": "nette/php-generator", - "version": "v3.6.5", + "version": "v3.6.9", "source": { "type": "git", "url": "https://github.com/nette/php-generator.git", - "reference": "9370403f9d9c25b51c4596ded1fbfe70347f7c82" + "reference": "d31782f7bd2ae84ad06f863391ec3fb77ca4d0a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/php-generator/zipball/9370403f9d9c25b51c4596ded1fbfe70347f7c82", - "reference": "9370403f9d9c25b51c4596ded1fbfe70347f7c82", + "url": "https://api.github.com/repos/nette/php-generator/zipball/d31782f7bd2ae84ad06f863391ec3fb77ca4d0a6", + "reference": "d31782f7bd2ae84ad06f863391ec3fb77ca4d0a6", "shasum": "" }, "require": { "nette/utils": "^3.1.2", - "php": ">=7.2 <8.2" + "php": ">=7.2 <8.3" }, "require-dev": { "nette/tester": "^2.4", @@ -1828,9 +1836,9 @@ ], "support": { "issues": "https://github.com/nette/php-generator/issues", - "source": "https://github.com/nette/php-generator/tree/v3.6.5" + "source": "https://github.com/nette/php-generator/tree/v3.6.9" }, - "time": "2021-11-24T16:23:44+00:00" + "time": "2022-10-04T11:49:47+00:00" }, { "name": "nette/robot-loader", @@ -1963,25 +1971,26 @@ }, { "name": "nette/utils", - "version": "v3.2.7", + "version": "v3.2.10", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "0af4e3de4df9f1543534beab255ccf459e7a2c99" + "reference": "a4175c62652f2300c8017fb7e640f9ccb11648d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/0af4e3de4df9f1543534beab255ccf459e7a2c99", - "reference": "0af4e3de4df9f1543534beab255ccf459e7a2c99", + "url": "https://api.github.com/repos/nette/utils/zipball/a4175c62652f2300c8017fb7e640f9ccb11648d2", + "reference": "a4175c62652f2300c8017fb7e640f9ccb11648d2", "shasum": "" }, "require": { - "php": ">=7.2 <8.2" + "php": ">=7.2 <8.4" }, "conflict": { "nette/di": "<3.0.6" }, "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", "nette/tester": "~2.0", "phpstan/phpstan": "^1.0", "tracy/tracy": "^2.3" @@ -2042,31 +2051,33 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v3.2.7" + "source": "https://github.com/nette/utils/tree/v3.2.10" }, - "time": "2022-01-24T11:29:14+00:00" + "time": "2023-07-30T15:38:18+00:00" }, { "name": "nikic/php-parser", - "version": "v4.19.1", + "version": "v5.6.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b" + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4e1b88d21c69391150ace211e9eaf05810858d0b", - "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.1" + "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" @@ -2074,7 +2085,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -2098,9 +2109,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.0" }, - "time": "2024-03-17T08:10:35+00:00" + "time": "2025-07-27T20:03:57+00:00" }, { "name": "ondram/ci-detector", @@ -2176,23 +2187,23 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.25.0.13", + "version": "6.57.0.0", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "ee473c36242850418a8bf372961ab3d9ec0ca234" + "reference": "dcc22b90a63497f3450dd5eed62197bc46937297" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/ee473c36242850418a8bf372961ab3d9ec0ca234", - "reference": "ee473c36242850418a8bf372961ab3d9ec0ca234", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/dcc22b90a63497f3450dd5eed62197bc46937297", + "reference": "dcc22b90a63497f3450dd5eed62197bc46937297", "shasum": "" }, "require": { "ext-json": "*", - "jetbrains/phpstorm-stubs": "dev-master#217ed9356d07ef89109d3cd7d8c5df10aab4b0d4", - "nikic/php-parser": "^4.18.0", - "php": "^7.2 || ^8.0" + "jetbrains/phpstorm-stubs": "dev-master#dfcad4524db603bd20bdec3aab1a31c5f5128ea3", + "nikic/php-parser": "^5.4.0", + "php": "^7.4 || ^8.0" }, "conflict": { "thecodingmachine/safe": "<1.1.3" @@ -2201,9 +2212,8 @@ "doctrine/coding-standard": "^12.0.0", "phpstan/phpstan": "^1.10.60", "phpstan/phpstan-phpunit": "^1.3.16", - "phpunit/phpunit": "^10.5.12", - "rector/rector": "0.14.3", - "vimeo/psalm": "5.23.0" + "phpunit/phpunit": "^11.5.7", + "rector/rector": "1.2.10" }, "suggest": { "composer/composer": "Required to use the ComposerSourceLocator" @@ -2242,22 +2252,144 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.25.0.13" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.57.0.0" + }, + "time": "2025-02-12T21:16:38+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": "2024-08-03T11:36:12+00:00" + "time": "2025-06-10T09:58:42+00:00" }, { "name": "phpstan/php-8-stubs", - "version": "0.3.95", + "version": "0.4.17", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "1e2422fdfc9da3e96bc1038eaf42728025d24756" + "reference": "87c8b96af4fb79bfa8180301fdbb150219befd25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/1e2422fdfc9da3e96bc1038eaf42728025d24756", - "reference": "1e2422fdfc9da3e96bc1038eaf42728025d24756", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/87c8b96af4fb79bfa8180301fdbb150219befd25", + "reference": "87c8b96af4fb79bfa8180301fdbb150219befd25", "shasum": "" }, "type": "library", @@ -2274,36 +2406,36 @@ "description": "PHP stubs extracted from php-src", "support": { "issues": "https://github.com/phpstan/php-8-stubs/issues", - "source": "https://github.com/phpstan/php-8-stubs/tree/0.3.95" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.17" }, - "time": "2024-08-12T00:18:17+00:00" + "time": "2025-08-09T00:23:06+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.29.1", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { "doctrine/annotations": "^2.0", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^5.3.0", "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", + "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", @@ -2321,9 +2453,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.2.0" }, - "time": "2024-05-31T08:52:43+00:00" + "time": "2025-07-13T07:04:09+00:00" }, { "name": "psr/container", @@ -2622,33 +2754,34 @@ }, { "name": "react/child-process", - "version": "v0.6.5", + "version": "0.7.x-dev", "source": { "type": "git", "url": "https://github.com/reactphp/child-process.git", - "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43" + "reference": "ce2654d21d2a749e0a6142d00432e65ba003a2d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/child-process/zipball/e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", - "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/ce2654d21d2a749e0a6142d00432e65ba003a2d9", + "reference": "ce2654d21d2a749e0a6142d00432e65ba003a2d9", "shasum": "" }, "require": { "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "php": ">=5.3.0", "react/event-loop": "^1.2", - "react/stream": "^1.2" + "react/stream": "^1.4" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", - "react/socket": "^1.8", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" }, + "default-branch": true, "type": "library", "autoload": { "psr-4": { - "React\\ChildProcess\\": "src" + "React\\ChildProcess\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2685,19 +2818,15 @@ ], "support": { "issues": "https://github.com/reactphp/child-process/issues", - "source": "https://github.com/reactphp/child-process/tree/v0.6.5" + "source": "https://github.com/reactphp/child-process/tree/0.7.x" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2022-09-16T13:41:56+00:00" + "time": "2024-08-04T20:30:51+00:00" }, { "name": "react/dns", @@ -3013,31 +3142,31 @@ }, { "name": "react/socket", - "version": "v1.15.0", + "version": "v1.16.0", "source": { "type": "git", "url": "https://github.com/reactphp/socket.git", - "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038" + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/216d3aec0b87f04a40ca04f481e6af01bdd1d038", - "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038", + "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.11", + "react/dns": "^1.13", "react/event-loop": "^1.2", - "react/promise": "^3 || ^2.6 || ^1.2.1", - "react/stream": "^1.2" + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" }, "require-dev": { "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/async": "^4 || ^3 || ^2", + "react/async": "^4.3 || ^3.3 || ^2", "react/promise-stream": "^1.4", - "react/promise-timer": "^1.10" + "react/promise-timer": "^1.11" }, "type": "library", "autoload": { @@ -3081,7 +3210,7 @@ ], "support": { "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.15.0" + "source": "https://github.com/reactphp/socket/tree/v1.16.0" }, "funding": [ { @@ -3089,7 +3218,7 @@ "type": "open_collective" } ], - "time": "2023-12-15T11:02:10+00:00" + "time": "2024-07-26T10:38:09+00:00" }, { "name": "react/stream", @@ -3169,18 +3298,85 @@ ], "time": "2024-06-11T12:45:25+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": "symfony/console", - "version": "v5.4.41", + "version": "v5.4.47", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6473d441a913cb997123b59ff2dbe3d1cf9e11ba" + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6473d441a913cb997123b59ff2dbe3d1cf9e11ba", - "reference": "6473d441a913cb997123b59ff2dbe3d1cf9e11ba", + "url": "https://api.github.com/repos/symfony/console/zipball/c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", "shasum": "" }, "require": { @@ -3250,7 +3446,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.41" + "source": "https://github.com/symfony/console/tree/v5.4.47" }, "funding": [ { @@ -3266,7 +3462,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T07:48:55+00:00" + "time": "2024-11-06T11:30:55+00:00" }, { "name": "symfony/deprecation-contracts", @@ -3287,12 +3483,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -3337,16 +3533,16 @@ }, { "name": "symfony/finder", - "version": "v5.4.40", + "version": "v5.4.45", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "f51cff4687547641c7d8180d74932ab40b2205ce" + "reference": "63741784cd7b9967975eec610b256eed3ede022b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/f51cff4687547641c7d8180d74932ab40b2205ce", - "reference": "f51cff4687547641c7d8180d74932ab40b2205ce", + "url": "https://api.github.com/repos/symfony/finder/zipball/63741784cd7b9967975eec610b256eed3ede022b", + "reference": "63741784cd7b9967975eec610b256eed3ede022b", "shasum": "" }, "require": { @@ -3380,7 +3576,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.40" + "source": "https://github.com/symfony/finder/tree/v5.4.45" }, "funding": [ { @@ -3396,24 +3592,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:33:22+00:00" + "time": "2024-09-28T13:32:08+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.30.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -3424,8 +3620,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -3459,7 +3655,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -3475,24 +3671,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.30.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", + "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" @@ -3500,8 +3696,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -3537,7 +3733,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -3553,24 +3749,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.30.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "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" @@ -3578,8 +3774,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -3618,7 +3814,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -3634,24 +3830,25 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.30.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "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": "*" @@ -3662,8 +3859,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -3698,7 +3895,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -3714,30 +3911,30 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:30:46+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { - "name": "symfony/polyfill-php73", - "version": "v1.30.0", + "name": "symfony/polyfill-php80", + "version": "v1.32.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/ec444d3f3f6505bb28d11afa41e75faadebc10a1", - "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -3745,7 +3942,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" + "Symfony\\Polyfill\\Php80\\": "" }, "classmap": [ "Resources/stubs" @@ -3756,6 +3953,10 @@ "MIT" ], "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -3765,7 +3966,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -3774,7 +3975,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" }, "funding": [ { @@ -3790,30 +3991,30 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { - "name": "symfony/polyfill-php74", - "version": "v1.30.0", + "name": "symfony/polyfill-php81", + "version": "v1.32.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php74.git", - "reference": "37f1d1a2fb3ebc494f9f9b0f7e92064b43332321" + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php74/zipball/37f1d1a2fb3ebc494f9f9b0f7e92064b43332321", - "reference": "37f1d1a2fb3ebc494f9f9b0f7e92064b43332321", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -3821,18 +4022,17 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php74\\": "" - } + "Symfony\\Polyfill\\Php81\\": "" + }, + "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" @@ -3842,7 +4042,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.4+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -3851,7 +4051,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php74/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0" }, "funding": [ { @@ -3867,41 +4067,33 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.30.0", + "name": "symfony/process", + "version": "v5.4.40", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" + "url": "https://github.com/symfony/process.git", + "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", + "url": "https://api.github.com/repos/symfony/process/zipball/deedcb3bb4669cae2148bc920eafd2b16dc7c046", + "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" }, "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" + "Symfony\\Component\\Process\\": "" }, - "classmap": [ - "Resources/stubs" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -3910,28 +4102,18 @@ ], "authors": [ { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "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 backporting some PHP 8.0+ features to lower PHP versions", + "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" + "source": "https://github.com/symfony/process/tree/v5.4.40" }, "funding": [ { @@ -3947,179 +4129,41 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-05-31T14:33:22+00:00" }, { - "name": "symfony/polyfill-php81", - "version": "v1.30.0", + "name": "symfony/service-contracts", + "version": "v2.5.4", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af" + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/3fb075789fb91f9ad9af537c4012d523085bd5af", - "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f37b419f7aea2e9abf10abd261832cace12e3300", + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300", "shasum": "" }, "require": { - "php": ">=7.1" + "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": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.30.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-06-19T12:30:46+00:00" - }, - { - "name": "symfony/process", - "version": "v5.4.40", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/deedcb3bb4669cae2148bc920eafd2b16dc7c046", - "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Executes commands in sub-processes", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v5.4.40" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-05-31T14:33:22+00:00" - }, - { - "name": "symfony/service-contracts", - "version": "v2.5.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a2329596ddc8fd568900e3fc76cba42489ecc7f3", - "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1|^3" - }, - "conflict": { - "ext-psr": "<1.1|>=2" - }, - "suggest": { - "symfony/service-implementation": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "2.5-dev" } }, "autoload": { @@ -4152,7 +4196,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.3" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.4" }, "funding": [ { @@ -4168,20 +4212,20 @@ "type": "tidelift" } ], - "time": "2023-04-21T15:04:16+00:00" + "time": "2024-09-25T14:11:13+00:00" }, { "name": "symfony/string", - "version": "v5.4.41", + "version": "v5.4.47", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "065a9611e0b1fd2197a867e1fb7f2238191b7096" + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/065a9611e0b1fd2197a867e1fb7f2238191b7096", - "reference": "065a9611e0b1fd2197a867e1fb7f2238191b7096", + "url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799", + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799", "shasum": "" }, "require": { @@ -4238,7 +4282,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.41" + "source": "https://github.com/symfony/string/tree/v5.4.47" }, "funding": [ { @@ -4254,103 +4298,10 @@ "type": "tidelift" } ], - "time": "2024-06-28T09:20:55+00:00" + "time": "2024-11-10T20:33:58+00:00" } ], "packages-dev": [ - { - "name": "brianium/paratest", - "version": "v6.6.3", - "source": { - "type": "git", - "url": "https://github.com/paratestphp/paratest.git", - "reference": "f2d781bb9136cda2f5e73ee778049e80ba681cf6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/f2d781bb9136cda2f5e73ee778049e80ba681cf6", - "reference": "f2d781bb9136cda2f5e73ee778049e80ba681cf6", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-simplexml": "*", - "jean85/pretty-package-versions": "^2.0.5", - "php": "^7.3 || ^8.0", - "phpunit/php-code-coverage": "^9.2.16", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-timer": "^5.0.3", - "phpunit/phpunit": "^9.5.23", - "sebastian/environment": "^5.1.4", - "symfony/console": "^5.4.9 || ^6.1.2", - "symfony/polyfill-php80": "^v1.26.0", - "symfony/process": "^5.4.8 || ^6.1.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9.0.0", - "ext-pcov": "*", - "ext-posix": "*", - "infection/infection": "^0.26.13", - "malukenho/mcbumpface": "^1.1.5", - "squizlabs/php_codesniffer": "^3.7.1", - "symfony/filesystem": "^5.4.9 || ^6.1.0", - "vimeo/psalm": "^4.26.0" - }, - "bin": [ - "bin/paratest", - "bin/paratest.bat", - "bin/paratest_for_phpstorm" - ], - "type": "library", - "autoload": { - "psr-4": { - "ParaTest\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Brian Scaturro", - "email": "scaturrob@gmail.com", - "role": "Developer" - }, - { - "name": "Filippo Tessarotto", - "email": "zoeslam@gmail.com", - "role": "Developer" - } - ], - "description": "Parallel testing for PHP", - "homepage": "https://github.com/paratestphp/paratest", - "keywords": [ - "concurrent", - "parallel", - "phpunit", - "testing" - ], - "support": { - "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v6.6.3" - }, - "funding": [ - { - "url": "https://github.com/sponsors/Slamdunk", - "type": "github" - }, - { - "url": "https://paypal.me/filippotessarotto", - "type": "paypal" - } - ], - "time": "2022-08-25T05:44:14+00:00" - }, { "name": "cweagans/composer-patches", "version": "1.7.3", @@ -4397,149 +4348,20 @@ "issues": "https://github.com/cweagans/composer-patches/issues", "source": "https://github.com/cweagans/composer-patches/tree/1.7.3" }, - "time": "2022-12-20T22:53:13+00:00" - }, - { - "name": "doctrine/instantiator", - "version": "1.4.1", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9", - "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.22" - }, - "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.1" - }, - "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-03-03T08:28:38+00:00" - }, - { - "name": "jean85/pretty-package-versions", - "version": "2.0.5", - "source": { - "type": "git", - "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/ae547e455a3d8babd07b96966b17d7fd21d9c6af", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af", - "shasum": "" - }, - "require": { - "composer-runtime-api": "^2.0.0", - "php": "^7.1|^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.17", - "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^0.12.66", - "phpunit/phpunit": "^7.5|^8.5|^9.4", - "vimeo/psalm": "^4.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Jean85\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Alessandro Lai", - "email": "alessandro.lai85@gmail.com" - } - ], - "description": "A library to get pretty versions strings of installed dependencies", - "keywords": [ - "composer", - "package", - "release", - "versions" - ], - "support": { - "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.5" - }, - "time": "2021-10-08T21:21:46+00:00" + "time": "2022-12-20T22:53:13+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.11.0", + "version": "1.13.3", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", "shasum": "" }, "require": { @@ -4547,11 +4369,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "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", @@ -4577,7 +4400,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" }, "funding": [ { @@ -4585,73 +4408,25 @@ "type": "tidelift" } ], - "time": "2022-03-03T13:19:32+00:00" - }, - { - "name": "ondrejmirtes/simple-downgrader", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/ondrejmirtes/simple-downgrader.git", - "reference": "832aaae53dcfe358f63180494de8734244773d46" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/simple-downgrader/zipball/832aaae53dcfe358f63180494de8734244773d46", - "reference": "832aaae53dcfe358f63180494de8734244773d46", - "shasum": "" - }, - "require": { - "nette/utils": "^3.2.5", - "nikic/php-parser": "^4.18", - "php": "^7.2|^8.0", - "phpstan/phpdoc-parser": "^1.24.5", - "symfony/console": "^5.4", - "symfony/finder": "^5.4" - }, - "require-dev": { - "php-parallel-lint/php-parallel-lint": "^1.3", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^8.5.36" - }, - "bin": [ - "bin/simple-downgrade" - ], - "type": "library", - "autoload": { - "psr-4": { - "SimpleDowngrader\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Simple Downgrader", - "support": { - "issues": "https://github.com/ondrejmirtes/simple-downgrader/issues", - "source": "https://github.com/ondrejmirtes/simple-downgrader/tree/1.0.2" - }, - "time": "2024-02-12T19:22:32+00:00" + "time": "2025-07-05T12:25:42+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "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", @@ -4692,9 +4467,15 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -4810,26 +4591,26 @@ }, { "name": "phpstan/phpstan-deprecation-rules", - "version": "1.2.0", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "fa8cce7720fa782899a0aa97b6a41225d1bb7b26" + "reference": "468e02c9176891cc901143da118f09dc9505fc2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/fa8cce7720fa782899a0aa97b6a41225d1bb7b26", - "reference": "fa8cce7720fa782899a0aa97b6a41225d1bb7b26", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/468e02c9176891cc901143da118f09dc9505fc2f", + "reference": "468e02c9176891cc901143da118f09dc9505fc2f", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.11" + "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" }, "type": "phpstan-extension", "extra": { @@ -4851,27 +4632,27 @@ "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", "support": { "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", - "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.2.0" + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.3" }, - "time": "2024-04-20T06:39:48+00:00" + "time": "2025-05-14T10:56:57+00:00" }, { "name": "phpstan/phpstan-nette", - "version": "1.3.8", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-nette.git", - "reference": "bc74b8b208b47f163fe55708fcf1a0333247fa79" + "reference": "9b629867b8e13e0afad8c8537b6541230d7b6a38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/bc74b8b208b47f163fe55708fcf1a0333247fa79", - "reference": "bc74b8b208b47f163fe55708fcf1a0333247fa79", + "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/9b629867b8e13e0afad8c8537b6541230d7b6a38", + "reference": "9b629867b8e13e0afad8c8537b6541230d7b6a38", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.11.11" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.12" }, "conflict": { "nette/application": "<2.3.0", @@ -4883,13 +4664,14 @@ }, "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.2", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "~9.5.28" + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" }, "type": "phpstan-extension", "extra": { @@ -4912,37 +4694,39 @@ "description": "Nette Framework class reflection extension for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-nette/issues", - "source": "https://github.com/phpstan/phpstan-nette/tree/1.3.8" + "source": "https://github.com/phpstan/phpstan-nette/tree/2.0.4" }, - "time": "2024-08-25T12:11:12+00:00" + "time": "2025-06-17T13:26:39+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.4.0", + "version": "2.0.x-dev", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "f3ea021866f4263f07ca3636bf22c64be9610c11" + "reference": "9a9b161baee88a5f5c58d816943cff354ff233dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/f3ea021866f4263f07ca3636bf22c64be9610c11", - "reference": "f3ea021866f4263f07ca3636bf22c64be9610c11", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/9a9b161baee88a5f5c58d816943cff354ff233dc", + "reference": "9a9b161baee88a5f5c58d816943cff354ff233dc", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.11" + "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.5.1", - "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": { "phpstan": { @@ -4964,34 +4748,33 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.4.0" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.x" }, - "time": "2024-04-20T06:39:00+00:00" + "time": "2025-07-13T11:31:46+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "1.6.0", + "version": "2.0.6", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "363f921dd8441777d4fc137deb99beb486c77df1" + "reference": "f9f77efa9de31992a832ff77ea52eb42d675b094" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/363f921dd8441777d4fc137deb99beb486c77df1", - "reference": "363f921dd8441777d4fc137deb99beb486c77df1", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/f9f77efa9de31992a832ff77ea52eb42d675b094", + "reference": "f9f77efa9de31992a832ff77ea52eb42d675b094", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.11" + "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-deprecation-rules": "^1.1", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" }, "type": "phpstan-extension", "extra": { @@ -5013,41 +4796,41 @@ "description": "Extra strict and opinionated rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.6.0" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.6" }, - "time": "2024-04-20T06:37:51+00:00" + "time": "2025-07-21T12:19:29+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.30", + "version": "11.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" + "reference": "1a800a7446add2d79cc6b3c01c45381810367d76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "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": "^4.18 || ^5.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" + "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": "PHP extension that provides line coverage", @@ -5056,7 +4839,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "11.0.x-dev" } }, "autoload": { @@ -5085,40 +4868,52 @@ "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.30" + "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": "2023-12-22T06:47:57+00:00" + "time": "2025-06-18T08:56:18+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.6", + "version": "5.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "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": { @@ -5145,7 +4940,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" }, "funding": [ { @@ -5153,28 +4949,28 @@ "type": "github" } ], - "time": "2021-12-02T12:48:52+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": "*" @@ -5182,7 +4978,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -5208,7 +5004,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": [ { @@ -5216,32 +5013,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": { @@ -5267,7 +5064,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": [ { @@ -5275,32 +5073,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": { @@ -5326,7 +5124,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": [ { @@ -5334,54 +5133,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.23", + "version": "11.5.28", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "888556852e7e9bbeeedb9656afe46118765ade34" + "reference": "93f30aa3889e785ac63493d4976df0ae9fdecb60" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/888556852e7e9bbeeedb9656afe46118765ade34", - "reference": "888556852e7e9bbeeedb9656afe46118765ade34", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/93f30aa3889e785ac63493d4976df0ae9fdecb60", + "reference": "93f30aa3889e785ac63493d4976df0ae9fdecb60", "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.3", - "phar-io/version": "^3.0.2", - "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.13", - "phpunit/php-file-iterator": "^3.0.5", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.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": "^3.0", - "sebastian/version": "^3.0.2" + "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": "*", - "ext-xdebug": "*" + "ext-soap": "To be able to generate mocks based on WSDL files" }, "bin": [ "phpunit" @@ -5389,7 +5186,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.5-dev" + "dev-main": "11.5-dev" } }, "autoload": { @@ -5420,7 +5217,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.23" + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.28" }, "funding": [ { @@ -5430,34 +5228,46 @@ { "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": "2022-08-22T14:01:36+00:00" + "time": "2025-07-31T07:10:28+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": { @@ -5480,7 +5290,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": [ { @@ -5488,32 +5299,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": { @@ -5536,7 +5347,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": [ { @@ -5544,32 +5356,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": { @@ -5591,7 +5403,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": [ { @@ -5599,34 +5412,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.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959", "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": { @@ -5665,7 +5483,8 @@ ], "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.1" }, "funding": [ { @@ -5673,33 +5492,33 @@ "type": "github" } ], - "time": "2020-10-26T15:49:45+00:00" + "time": "2025-03-07T06:57:01+00:00" }, { "name": "sebastian/complexity", - "version": "2.0.3", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "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": { @@ -5722,73 +5541,8 @@ "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.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": [ { @@ -5796,27 +5550,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.5", + "version": "7.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "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": "*" @@ -5824,7 +5578,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-main": "7.2-dev" } }, "autoload": { @@ -5843,7 +5597,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", @@ -5851,42 +5605,55 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + "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": "2023-02-03T06:03:51+00:00" + "time": "2025-05-21T11:55:47+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.4", + "version": "6.3.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "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": { @@ -5928,7 +5695,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" }, "funding": [ { @@ -5936,38 +5704,35 @@ "type": "github" } ], - "time": "2021-11-11T14:18:36+00:00" + "time": "2024-12-05T09:17:50+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.5", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "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": { @@ -5986,13 +5751,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.5" + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" }, "funding": [ { @@ -6000,33 +5766,33 @@ "type": "github" } ], - "time": "2022-02-14T08:28:10+00:00" + "time": "2024-07-03T04:57:36+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.4", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "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": { @@ -6049,7 +5815,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.4" + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" }, "funding": [ { @@ -6057,34 +5824,34 @@ "type": "github" } ], - "time": "2023-12-22T06:20:34+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": { @@ -6106,7 +5873,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": [ { @@ -6114,32 +5882,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": { @@ -6161,7 +5929,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": [ { @@ -6169,32 +5938,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": { @@ -6221,65 +5990,11 @@ } ], "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" - }, - "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": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" }, "funding": [ { @@ -6287,32 +6002,32 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-07-03T05:10:34+00:00" }, { "name": "sebastian/type", - "version": "3.0.0", + "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad" + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -6335,7 +6050,8 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.2" }, "funding": [ { @@ -6343,29 +6059,29 @@ "type": "github" } ], - "time": "2022-03-15T09:54:48+00:00" + "time": "2025-03-18T13:35:50+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": { @@ -6388,7 +6104,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": [ { @@ -6396,7 +6113,7 @@ "type": "github" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2024-10-09T05:16:32+00:00" }, { "name": "shipmonk/composer-dependency-analyser", @@ -6464,6 +6181,81 @@ }, "time": "2024-08-08T08:12:32+00:00" }, + { + "name": "shipmonk/dead-code-detector", + "version": "0.12.2", + "source": { + "type": "git", + "url": "https://github.com/shipmonk-rnd/dead-code-detector.git", + "reference": "71b842269e9a29634e34074e723023e4e151518b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shipmonk-rnd/dead-code-detector/zipball/71b842269e9a29634e34074e723023e4e151518b", + "reference": "71b842269e9a29634e34074e723023e4e151518b", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.9" + }, + "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": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "ShipMonk\\PHPStan\\DeadCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "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": [ + "PHPStan", + "dead code", + "static analysis", + "unused code" + ], + "support": { + "issues": "https://github.com/shipmonk-rnd/dead-code-detector/issues", + "source": "https://github.com/shipmonk-rnd/dead-code-detector/tree/0.12.2" + }, + "time": "2025-05-22T07:50:57+00:00" + }, { "name": "shipmonk/name-collision-detector", "version": "2.1.1", @@ -6522,18 +6314,70 @@ }, "time": "2024-03-01T13:26: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": "theseer/tokenizer", - "version": "1.2.1", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -6562,7 +6406,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/1.2.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -6570,7 +6414,7 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], @@ -6581,12 +6425,12 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.1", + "php": "^8.2", "composer-runtime-api": "^2.0" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { - "php": "8.1.99" + "php": "8.2.99" }, "plugin-api-version": "2.6.0" } diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index a924d8222a..1c0fec310f 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -1,64 +1,10 @@ parameters: featureToggles: bleedingEdge: true + checkNonStringableDynamicAccess: true + checkParameterCastableToNumberFunctions: true skipCheckGenericClasses!: [] - explicitMixedInUnknownGenericNew: true - explicitMixedForGlobalVariables: true - explicitMixedViaIsArray: true - arrayFilter: true - arrayUnpacking: true - arrayValues: true - nodeConnectingVisitorCompatibility: false - nodeConnectingVisitorRule: true - disableCheckMissingIterableValueType: true - strictUnnecessaryNullsafePropertyFetch: true - looseComparison: true - consistentConstructor: true - checkUnresolvableParameterTypes: true - readOnlyByPhpDoc: true - phpDocParserRequireWhitespaceBeforeDescription: true - phpDocParserIncludeLines: true - enableIgnoreErrorsWithinPhpDocs: true - runtimeReflectionRules: true - notAnalysedTrait: true - curlSetOptTypes: true - listType: true - abstractTraitMethod: true - missingMagicSerializationRule: true - nullContextForVoidReturningFunctions: true - unescapeStrings: true - alwaysCheckTooWideReturnTypeFinalMethods: true - duplicateStubs: true - logicalXor: true - betterNoop: true - invarianceComposition: true - alwaysTrueAlwaysReported: true - disableUnreachableBranchesRules: true - varTagType: true - closureDefaultParameterTypeRule: true - newRuleLevelHelper: true - instanceofType: true - paramOutVariance: true - allInvalidPhpDocs: true - strictStaticMethodTemplateTypeVariance: true - propertyVariance: true - genericPrototypeMessage: true stricterFunctionMap: true - invalidPhpDocTagLine: true - detectDeadTypeInMultiCatch: true - zeroFiles: true - projectServicesNotInAnalysedPaths: true - callUserFunc: true - finalByPhpDoc: true - magicConstantOutOfContext: true - paramOutType: true - pure: true - checkParameterCastableToStringFunctions: true - narrowPregMatches: true - uselessReturnValue: true - printfArrayParameters: true - preciseMissingReturn: true - validatePregQuote: true - noImplicitWildcard: true - stubFiles: - - ../stubs/bleedingEdge/Rule.stub + reportPreciseLineForUnusedFunctionParameter: true + internalTag: true + newStaticInAbstractClassStaticMethod: true diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 8052e5f5de..805ea348a9 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -1,311 +1,29 @@ parameters: customRulesetUsed: false -conditionalTags: - PHPStan\Rules\Api\NodeConnectingVisitorAttributesRule: - phpstan.rules.rule: %featureToggles.nodeConnectingVisitorRule% - PHPStan\Rules\Properties\MissingReadOnlyByPhpDocPropertyAssignRule: - phpstan.rules.rule: %featureToggles.readOnlyByPhpDoc% - PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyRule: - phpstan.rules.rule: %featureToggles.readOnlyByPhpDoc% - PHPStan\Rules\Properties\UninitializedPropertyRule: - phpstan.rules.rule: %checkUninitializedProperties% - PHPStan\Rules\Methods\ConsistentConstructorRule: - phpstan.rules.rule: %featureToggles.consistentConstructor% - PHPStan\Rules\Api\ApiClassConstFetchRule: - phpstan.rules.rule: %featureToggles.runtimeReflectionRules% - PHPStan\Rules\Api\ApiInstanceofRule: - phpstan.rules.rule: %featureToggles.runtimeReflectionRules% - PHPStan\Rules\Api\RuntimeReflectionFunctionRule: - phpstan.rules.rule: %featureToggles.runtimeReflectionRules% - PHPStan\Rules\Api\RuntimeReflectionInstantiationRule: - phpstan.rules.rule: %featureToggles.runtimeReflectionRules% - PHPStan\Rules\Methods\MissingMagicSerializationMethodsRule: - phpstan.rules.rule: %featureToggles.missingMagicSerializationRule% - PHPStan\Rules\Constants\MagicConstantContextRule: - phpstan.rules.rule: %featureToggles.magicConstantOutOfContext% - PHPStan\Rules\Functions\UselessFunctionReturnValueRule: - phpstan.rules.rule: %featureToggles.uselessReturnValue% - PHPStan\Rules\Functions\PrintfArrayParametersRule: - phpstan.rules.rule: %featureToggles.printfArrayParameters% - PHPStan\Rules\Regexp\RegularExpressionQuotingRule: - phpstan.rules.rule: %featureToggles.validatePregQuote% +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\GetTemplateTypeRule - - PHPStan\Rules\Api\PhpStanNamespaceIn3rdPartyPackageRule - - PHPStan\Rules\Arrays\DuplicateKeysInLiteralArraysRule - - PHPStan\Rules\Arrays\EmptyArrayItemRule - - PHPStan\Rules\Arrays\OffsetAccessWithoutDimForReadingRule - - PHPStan\Rules\Cast\UnsetCastRule - - PHPStan\Rules\Classes\AllowedSubTypesRule - - PHPStan\Rules\Classes\ClassAttributesRule - - PHPStan\Rules\Classes\ClassConstantAttributesRule - - PHPStan\Rules\Classes\ClassConstantRule - - PHPStan\Rules\Classes\DuplicateDeclarationRule - - PHPStan\Rules\Classes\EnumSanityRule - - PHPStan\Rules\Classes\ExistingClassesInClassImplementsRule - - PHPStan\Rules\Classes\ExistingClassesInEnumImplementsRule - - PHPStan\Rules\Classes\ExistingClassesInInterfaceExtendsRule - - PHPStan\Rules\Classes\ExistingClassInTraitUseRule - - PHPStan\Rules\Classes\InstantiationRule - - PHPStan\Rules\Classes\InstantiationCallableRule - - PHPStan\Rules\Classes\InvalidPromotedPropertiesRule - - PHPStan\Rules\Classes\LocalTypeAliasesRule - - PHPStan\Rules\Classes\LocalTypeTraitAliasesRule - - PHPStan\Rules\Classes\NewStaticRule - - PHPStan\Rules\Classes\NonClassAttributeClassRule - - PHPStan\Rules\Classes\ReadOnlyClassRule - - PHPStan\Rules\Classes\TraitAttributeClassRule - - PHPStan\Rules\Constants\DynamicClassConstantFetchRule - - PHPStan\Rules\Constants\FinalConstantRule - - PHPStan\Rules\Constants\NativeTypedClassConstantRule - - PHPStan\Rules\EnumCases\EnumCaseAttributesRule - - PHPStan\Rules\Exceptions\NoncapturingCatchRule - - PHPStan\Rules\Exceptions\ThrowExpressionRule - - PHPStan\Rules\Functions\ArrowFunctionAttributesRule - - PHPStan\Rules\Functions\ArrowFunctionReturnNullsafeByRefRule - - PHPStan\Rules\Functions\ClosureAttributesRule - - PHPStan\Rules\Functions\DefineParametersRule - - PHPStan\Rules\Functions\ExistingClassesInArrowFunctionTypehintsRule - - PHPStan\Rules\Functions\CallToFunctionParametersRule - - PHPStan\Rules\Functions\ExistingClassesInClosureTypehintsRule - - PHPStan\Rules\Functions\ExistingClassesInTypehintsRule - - PHPStan\Rules\Functions\FunctionAttributesRule - - PHPStan\Rules\Functions\InnerFunctionRule - - PHPStan\Rules\Functions\InvalidLexicalVariablesInClosureUseRule - - PHPStan\Rules\Functions\ParamAttributesRule - - PHPStan\Rules\Functions\PrintfParametersRule - - PHPStan\Rules\Functions\RedefinedParametersRule - - PHPStan\Rules\Functions\ReturnNullsafeByRefRule - - PHPStan\Rules\Ignore\IgnoreParseErrorRule - - PHPStan\Rules\Functions\VariadicParametersDeclarationRule - - PHPStan\Rules\Keywords\ContinueBreakInLoopRule - - PHPStan\Rules\Keywords\DeclareStrictTypesRule - - PHPStan\Rules\Methods\AbstractMethodInNonAbstractClassRule - - PHPStan\Rules\Methods\AbstractPrivateMethodRule - - PHPStan\Rules\Methods\CallMethodsRule - - PHPStan\Rules\Methods\CallStaticMethodsRule - - PHPStan\Rules\Methods\ConstructorReturnTypeRule - - PHPStan\Rules\Methods\ExistingClassesInTypehintsRule - - PHPStan\Rules\Methods\FinalPrivateMethodRule - - PHPStan\Rules\Methods\MethodCallableRule - - PHPStan\Rules\Methods\MethodVisibilityInInterfaceRule - - PHPStan\Rules\Methods\MissingMethodImplementationRule - - PHPStan\Rules\Methods\MethodAttributesRule - - PHPStan\Rules\Methods\StaticMethodCallableRule - - PHPStan\Rules\Names\UsedNamesRule - - PHPStan\Rules\Operators\InvalidAssignVarRule - - PHPStan\Rules\Properties\AccessPropertiesInAssignRule - - PHPStan\Rules\Properties\AccessStaticPropertiesInAssignRule - - PHPStan\Rules\Properties\InvalidCallablePropertyTypeRule - - PHPStan\Rules\Properties\MissingReadOnlyPropertyAssignRule - - PHPStan\Rules\Properties\PropertiesInInterfaceRule - - PHPStan\Rules\Properties\PropertyAttributesRule - - PHPStan\Rules\Properties\ReadOnlyPropertyRule - - PHPStan\Rules\Traits\ConflictingTraitConstantsRule - - PHPStan\Rules\Traits\ConstantsInTraitsRule - - PHPStan\Rules\Types\InvalidTypesInUnionRule - - PHPStan\Rules\Variables\UnsetRule - - PHPStan\Rules\Whitespace\FileWhitespaceRule +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\Api\ApiClassConstFetchRule - - - class: PHPStan\Rules\Api\ApiInstanceofRule - - - class: PHPStan\Rules\Api\ApiInstanceofTypeRule - arguments: - enabled: %featureToggles.instanceofType% - deprecationRulesInstalled: %deprecationRulesInstalled% - tags: - - phpstan.rules.rule - - - class: PHPStan\Rules\Api\NodeConnectingVisitorAttributesRule - - - class: PHPStan\Rules\Api\RuntimeReflectionFunctionRule - - - class: PHPStan\Rules\Api\RuntimeReflectionInstantiationRule - - - 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\Constants\OverridingConstantRule - arguments: - checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Methods\OverridingMethodRule - arguments: - checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures% - genericPrototypeMessage: %featureToggles.genericPrototypeMessage% - finalByPhpDoc: %featureToggles.finalByPhpDoc% - checkMissingOverrideMethodAttribute: %checkMissingOverrideMethodAttribute% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Methods\ConsistentConstructorRule - - - - 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: - bleedingEdge: %featureToggles.bleedingEdge% - checkThisOnly: %checkThisOnly% - - - - class: PHPStan\Rules\Properties\AccessPropertiesRule - tags: - - phpstan.rules.rule - arguments: - reportMagicProperties: %reportMagicProperties% - checkDynamicProperties: %checkDynamicProperties% - - - - 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\Functions\FunctionCallableRule - arguments: - checkFunctionNameCase: %checkFunctionNameCase% - reportMaybes: %reportMaybes% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Properties\MissingReadOnlyByPhpDocPropertyAssignRule - - - - class: PHPStan\Rules\Properties\OverridingPropertyRule - arguments: - checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures% - reportMaybes: %reportMaybesInPropertyPhpDocTypes% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyRule - - - - class: PHPStan\Rules\Properties\UninitializedPropertyRule - - - - 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\Variables\DefinedVariableRule - arguments: - cliArgumentsVariablesRegistered: %cliArgumentsVariablesRegistered% - checkMaybeUndefinedVariables: %checkMaybeUndefinedVariables% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Regexp\RegularExpressionPatternRule - tags: - - phpstan.rules.rule - - - - class: PHPStan\Reflection\ConstructorsHelper - arguments: - additionalConstructors: %additionalConstructors% - - - - class: PHPStan\Rules\Methods\MissingMagicSerializationMethodsRule - - - - class: PHPStan\Rules\Constants\MagicConstantContextRule + class: PHPStan\Rules\Classes\NewStaticInAbstractClassStaticMethodRule - - class: PHPStan\Rules\Functions\UselessFunctionReturnValueRule + class: PHPStan\Rules\InternalTag\RestrictedInternalClassConstantUsageExtension - - class: PHPStan\Rules\Functions\PrintfArrayParametersRule + class: PHPStan\Rules\InternalTag\RestrictedInternalClassNameUsageExtension - - class: PHPStan\Rules\Regexp\RegularExpressionQuotingRule + 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 907c83e394..dd50138b54 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -6,145 +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\Constants\ValueAssignedToClassConstantRule - - PHPStan\Rules\Functions\IncompatibleDefaultParameterTypeRule - - PHPStan\Rules\Generics\ClassAncestorsRule - - PHPStan\Rules\Generics\ClassTemplateTypeRule - - PHPStan\Rules\Generics\EnumAncestorsRule - - PHPStan\Rules\Generics\EnumTemplateTypeRule - - PHPStan\Rules\Generics\FunctionTemplateTypeRule - - PHPStan\Rules\Generics\FunctionSignatureVarianceRule - - PHPStan\Rules\Generics\InterfaceAncestorsRule - - PHPStan\Rules\Generics\InterfaceTemplateTypeRule - - PHPStan\Rules\Generics\MethodTemplateTypeRule - - PHPStan\Rules\Generics\MethodTagTemplateTypeRule - - PHPStan\Rules\Generics\MethodSignatureVarianceRule - - PHPStan\Rules\Generics\TraitTemplateTypeRule - - PHPStan\Rules\Generics\UsedTraitsRule - - PHPStan\Rules\Methods\CallPrivateMethodThroughStaticRule - - PHPStan\Rules\Methods\IncompatibleDefaultParameterTypeRule - - PHPStan\Rules\Operators\InvalidComparisonOperationRule - - PHPStan\Rules\PhpDoc\FunctionConditionalReturnTypeRule - - PHPStan\Rules\PhpDoc\MethodConditionalReturnTypeRule - - PHPStan\Rules\PhpDoc\FunctionAssertRule - - PHPStan\Rules\PhpDoc\MethodAssertRule - - PHPStan\Rules\PhpDoc\IncompatibleSelfOutTypeRule - - PHPStan\Rules\PhpDoc\IncompatibleClassConstantPhpDocTypeRule - - PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule - - PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule - - PHPStan\Rules\PhpDoc\InvalidThrowsPhpDocValueRule - - PHPStan\Rules\Properties\AccessPrivatePropertyThroughStaticRule - - PHPStan\Rules\Classes\RequireImplementsRule - - PHPStan\Rules\Classes\RequireExtendsRule - - PHPStan\Rules\PhpDoc\RequireImplementsDefinitionClassRule - - PHPStan\Rules\PhpDoc\RequireExtendsDefinitionClassRule - - PHPStan\Rules\PhpDoc\RequireExtendsDefinitionTraitRule +autowiredAttributeServices: + # registers rules with #[RegisteredRule] attribute + level: 2 conditionalTags: - PHPStan\Rules\Functions\IncompatibleArrowFunctionDefaultParameterTypeRule: - phpstan.rules.rule: %featureToggles.closureDefaultParameterTypeRule% - PHPStan\Rules\Functions\IncompatibleClosureDefaultParameterTypeRule: - phpstan.rules.rule: %featureToggles.closureDefaultParameterTypeRule% - PHPStan\Rules\Methods\IllegalConstructorMethodCallRule: - phpstan.rules.rule: %featureToggles.illegalConstructorMethodCall% - PHPStan\Rules\Methods\IllegalConstructorStaticCallRule: - phpstan.rules.rule: %featureToggles.illegalConstructorMethodCall% - PHPStan\Rules\PhpDoc\VarTagChangedExpressionTypeRule: - phpstan.rules.rule: %featureToggles.varTagType% - PHPStan\Rules\Generics\PropertyVarianceRule: - phpstan.rules.rule: %featureToggles.propertyVariance% - PHPStan\Rules\Pure\PureFunctionRule: - phpstan.rules.rule: %featureToggles.pure% - PHPStan\Rules\Pure\PureMethodRule: - phpstan.rules.rule: %featureToggles.pure% + 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\InternalTag\RestrictedInternalPropertyUsageExtension - - class: PHPStan\Rules\PhpDoc\RequireExtendsCheck - arguments: - checkClassCaseSensitivity: %checkClassCaseSensitivity% - - - - class: PHPStan\Rules\PhpDoc\RequireImplementsDefinitionTraitRule - arguments: - checkClassCaseSensitivity: %checkClassCaseSensitivity% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Functions\IncompatibleArrowFunctionDefaultParameterTypeRule - - - class: PHPStan\Rules\Functions\IncompatibleClosureDefaultParameterTypeRule - - - class: PHPStan\Rules\Functions\CallCallablesRule - arguments: - reportMaybes: %reportMaybes% - tags: - - phpstan.rules.rule - - - class: PHPStan\Rules\Methods\IllegalConstructorMethodCallRule - - - class: PHPStan\Rules\Methods\IllegalConstructorStaticCallRule - - - class: PHPStan\Rules\PhpDoc\InvalidPhpDocTagValueRule - arguments: - checkAllInvalidPhpDocs: %featureToggles.allInvalidPhpDocs% - invalidPhpDocTagLine: %featureToggles.invalidPhpDocTagLine% - tags: - - phpstan.rules.rule - - - class: PHPStan\Rules\PhpDoc\InvalidPhpDocVarTagTypeRule - arguments: - checkClassCaseSensitivity: %checkClassCaseSensitivity% - checkMissingVarTagTypehint: %checkMissingVarTagTypehint% - tags: - - phpstan.rules.rule - - - class: PHPStan\Rules\PhpDoc\InvalidPHPStanDocTagRule - arguments: - checkAllInvalidPhpDocs: %featureToggles.allInvalidPhpDocs% - tags: - - phpstan.rules.rule - - - class: PHPStan\Rules\PhpDoc\VarTagChangedExpressionTypeRule - - - class: PHPStan\Rules\PhpDoc\WrongVariableNameInVarTagRule - arguments: - checkTypeAgainstNativeType: %featureToggles.varTagType% - tags: - - phpstan.rules.rule - - - class: PHPStan\Rules\Generics\PropertyVarianceRule - arguments: - readOnlyByPhpDoc: %featureToggles.readOnlyByPhpDoc% - - - class: PHPStan\Rules\Pure\PureFunctionRule - - - - class: PHPStan\Rules\Pure\PureMethodRule - - - class: PHPStan\Rules\Operators\InvalidBinaryOperationRule - arguments: - bleedingEdge: %featureToggles.bleedingEdge% - tags: - - phpstan.rules.rule - - - class: PHPStan\Rules\Operators\InvalidUnaryOperationRule - arguments: - bleedingEdge: %featureToggles.bleedingEdge% - tags: - - phpstan.rules.rule + class: PHPStan\Rules\InternalTag\RestrictedInternalMethodUsageExtension diff --git a/conf/config.level3.neon b/conf/config.level3.neon index 5540500714..b040bf3aeb 100644 --- a/conf/config.level3.neon +++ b/conf/config.level3.neon @@ -1,104 +1,9 @@ includes: - config.level2.neon -conditionalTags: - PHPStan\Rules\Arrays\ArrayUnpackingRule: - phpstan.rules.rule: %featureToggles.arrayUnpacking% - PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyAssignRefRule: - phpstan.rules.rule: %featureToggles.readOnlyByPhpDoc% - PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyAssignRule: - phpstan.rules.rule: %featureToggles.readOnlyByPhpDoc% - PHPStan\Rules\Variables\ParameterOutAssignedTypeRule: - phpstan.rules.rule: %featureToggles.paramOutType% - PHPStan\Rules\Variables\ParameterOutExecutionEndTypeRule: - phpstan.rules.rule: %featureToggles.paramOutType% - -rules: - - PHPStan\Rules\Arrays\ArrayDestructuringRule - - PHPStan\Rules\Arrays\IterableInForeachRule - - PHPStan\Rules\Arrays\OffsetAccessAssignmentRule - - PHPStan\Rules\Arrays\OffsetAccessAssignOpRule - - PHPStan\Rules\Arrays\OffsetAccessValueAssignmentRule - - PHPStan\Rules\Arrays\UnpackIterableInArrayRule - - PHPStan\Rules\Exceptions\ThrowExprTypeRule - - PHPStan\Rules\Functions\ArrowFunctionReturnTypeRule - - PHPStan\Rules\Functions\ClosureReturnTypeRule - - PHPStan\Rules\Functions\ReturnTypeRule - - PHPStan\Rules\Generators\YieldTypeRule - - PHPStan\Rules\Methods\ReturnTypeRule - - PHPStan\Rules\Properties\DefaultValueTypesAssignedToPropertiesRule - - PHPStan\Rules\Properties\ReadOnlyPropertyAssignRule - - PHPStan\Rules\Properties\ReadOnlyPropertyAssignRefRule - - 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\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\Generators\YieldFromTypeRule - arguments: - reportMaybes: %reportMaybes% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Generators\YieldInGeneratorRule - arguments: - reportMaybes: %reportMaybes% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Arrays\ArrayUnpackingRule - - - - class: PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyAssignRefRule - - - - class: PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyAssignRule - - - - class: PHPStan\Rules\Variables\ParameterOutAssignedTypeRule - - - - class: PHPStan\Rules\Variables\ParameterOutExecutionEndTypeRule diff --git a/conf/config.level4.neon b/conf/config.level4.neon index cb79c9cca5..cffef56a11 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -1,315 +1,27 @@ includes: - config.level3.neon -rules: - - PHPStan\Rules\Arrays\DeadForeachRule - - PHPStan\Rules\DeadCode\UnreachableStatementRule - - PHPStan\Rules\DeadCode\UnusedPrivateConstantRule - - PHPStan\Rules\DeadCode\UnusedPrivateMethodRule - - PHPStan\Rules\Exceptions\OverwrittenExitPointByFinallyRule - - PHPStan\Rules\Functions\CallToFunctionStatementWithoutSideEffectsRule - - PHPStan\Rules\Methods\CallToMethodStatementWithoutSideEffectsRule - - PHPStan\Rules\Methods\CallToStaticMethodStatementWithoutSideEffectsRule - - PHPStan\Rules\Methods\NullsafeMethodCallRule - - 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\Comparison\ConstantLooseComparisonRule: - phpstan.rules.rule: %featureToggles.looseComparison% - PHPStan\Rules\Traits\TraitDeclarationCollector: - phpstan.collector: %featureToggles.notAnalysedTrait% - PHPStan\Rules\Traits\TraitUseCollector: - phpstan.collector: %featureToggles.notAnalysedTrait% - PHPStan\Rules\Traits\NotAnalysedTraitRule: - phpstan.rules.rule: %featureToggles.notAnalysedTrait% - PHPStan\Rules\Comparison\LogicalXorConstantConditionRule: - phpstan.rules.rule: %featureToggles.logicalXor% - PHPStan\Rules\DeadCode\BetterNoopRule: - phpstan.rules.rule: %featureToggles.betterNoop% - PHPStan\Rules\TooWideTypehints\TooWideFunctionParameterOutTypeRule: - phpstan.rules.rule: %featureToggles.paramOutType% - PHPStan\Rules\TooWideTypehints\TooWideMethodParameterOutTypeRule: - phpstan.rules.rule: %featureToggles.paramOutType% - PHPStan\Rules\DeadCode\CallToConstructorStatementWithoutImpurePointsRule: - phpstan.rules.rule: %featureToggles.pure% - PHPStan\Rules\DeadCode\PossiblyPureNewCollector: - phpstan.collector: %featureToggles.pure% - PHPStan\Rules\DeadCode\ConstructorWithoutImpurePointsCollector: - phpstan.collector: %featureToggles.pure% - PHPStan\Rules\DeadCode\CallToFunctionStatementWithoutImpurePointsRule: - phpstan.rules.rule: %featureToggles.pure% - PHPStan\Rules\DeadCode\PossiblyPureFuncCallCollector: - phpstan.collector: %featureToggles.pure% - PHPStan\Rules\DeadCode\FunctionWithoutImpurePointsCollector: - phpstan.collector: %featureToggles.pure% - PHPStan\Rules\DeadCode\CallToMethodStatementWithoutImpurePointsRule: - phpstan.rules.rule: %featureToggles.pure% - PHPStan\Rules\DeadCode\PossiblyPureMethodCallCollector: - phpstan.collector: %featureToggles.pure% - PHPStan\Rules\DeadCode\MethodWithoutImpurePointsCollector: - phpstan.collector: %featureToggles.pure% - PHPStan\Rules\DeadCode\CallToStaticMethodStatementWithoutImpurePointsRule: - phpstan.rules.rule: %featureToggles.pure% - PHPStan\Rules\DeadCode\PossiblyPureStaticCallCollector: - phpstan.collector: %featureToggles.pure% + PHPStan\Rules\Exceptions\TooWideFunctionThrowTypeRule: + phpstan.rules.rule: %exceptions.check.tooWideThrowType% + PHPStan\Rules\Exceptions\TooWideMethodThrowTypeRule: + phpstan.rules.rule: %exceptions.check.tooWideThrowType% + PHPStan\Rules\Exceptions\TooWidePropertyHookThrowTypeRule: + phpstan.rules.rule: %exceptions.check.tooWideThrowType% parameters: checkAdvancedIsset: true services: - - class: PHPStan\Rules\Classes\ImpossibleInstanceOfRule - arguments: - checkAlwaysTrueInstanceof: %checkAlwaysTrueInstanceof% - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% - tags: - - phpstan.rules.rule + class: PHPStan\Rules\Exceptions\TooWideFunctionThrowTypeRule - - class: PHPStan\Rules\Comparison\BooleanAndConstantConditionRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - bleedingEdge: %featureToggles.bleedingEdge% - reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% - tags: - - phpstan.rules.rule + class: PHPStan\Rules\Exceptions\TooWideMethodThrowTypeRule - - class: PHPStan\Rules\Comparison\BooleanOrConstantConditionRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - bleedingEdge: %featureToggles.bleedingEdge% - reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\BooleanNotConstantConditionRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\DeadCode\NoopRule - arguments: - better: %featureToggles.betterNoop% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\DeadCode\CallToConstructorStatementWithoutImpurePointsRule - - - - class: PHPStan\Rules\DeadCode\ConstructorWithoutImpurePointsCollector - - - - class: PHPStan\Rules\DeadCode\PossiblyPureNewCollector - - - - class: PHPStan\Rules\DeadCode\CallToFunctionStatementWithoutImpurePointsRule - - - - class: PHPStan\Rules\DeadCode\FunctionWithoutImpurePointsCollector - - - - class: PHPStan\Rules\DeadCode\PossiblyPureFuncCallCollector - - - - class: PHPStan\Rules\DeadCode\CallToMethodStatementWithoutImpurePointsRule - - - - class: PHPStan\Rules\DeadCode\MethodWithoutImpurePointsCollector - - - - class: PHPStan\Rules\DeadCode\PossiblyPureMethodCallCollector - - - - class: PHPStan\Rules\DeadCode\CallToStaticMethodStatementWithoutImpurePointsRule - - - - class: PHPStan\Rules\DeadCode\PossiblyPureStaticCallCollector - - - - 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% - reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% - 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% - reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\ImpossibleCheckTypeMethodCallRule - arguments: - checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall% - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\ImpossibleCheckTypeStaticMethodCallRule - arguments: - checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall% - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\LogicalXorConstantConditionRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% - - - - class: PHPStan\Rules\DeadCode\BetterNoopRule - - - - class: PHPStan\Rules\Comparison\MatchExpressionRule - arguments: - checkAlwaysTrueStrictComparison: %checkAlwaysTrueStrictComparison% - disableUnreachable: %featureToggles.disableUnreachableBranchesRules% - reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\NumberComparisonOperatorsConstantConditionRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\StrictComparisonOfDifferentTypesRule - arguments: - checkAlwaysTrueStrictComparison: %checkAlwaysTrueStrictComparison% - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\ConstantLooseComparisonRule - arguments: - checkAlwaysTrueLooseComparison: %checkAlwaysTrueLooseComparison% - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% - - - - class: PHPStan\Rules\Comparison\TernaryOperatorConstantConditionRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\UnreachableIfBranchesRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - disable: %featureToggles.disableUnreachableBranchesRules% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Comparison\UnreachableTernaryElseBranchRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - disable: %featureToggles.disableUnreachableBranchesRules% - 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\Methods\CallToConstructorStatementWithoutSideEffectsRule - arguments: - reportNoConstructor: %featureToggles.pure% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\TooWideTypehints\TooWideMethodReturnTypehintRule - arguments: - checkProtectedAndPublicMethods: %checkTooWideReturnTypesInProtectedAndPublicMethods% - alwaysCheckFinal: %featureToggles.alwaysCheckTooWideReturnTypeFinalMethods% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Properties\NullsafePropertyFetchRule - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Traits\TraitDeclarationCollector - - - - class: PHPStan\Rules\Traits\TraitUseCollector - - - - class: PHPStan\Rules\Traits\NotAnalysedTraitRule - - - - class: PHPStan\Rules\Exceptions\CatchWithUnthrownExceptionRule - arguments: - exceptionTypeResolver: @exceptionTypeResolver - reportUncheckedExceptionDeadCatch: %exceptions.reportUncheckedExceptionDeadCatch% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\TooWideTypehints\TooWideFunctionParameterOutTypeRule - - - - class: PHPStan\Rules\TooWideTypehints\TooWideMethodParameterOutTypeRule + class: PHPStan\Rules\Exceptions\TooWidePropertyHookThrowTypeRule diff --git a/conf/config.level5.neon b/conf/config.level5.neon index 184cee83b8..4d9bbeedf4 100644 --- a/conf/config.level5.neon +++ b/conf/config.level5.neon @@ -6,51 +6,13 @@ parameters: checkArgumentsPassedByReference: true conditionalTags: - PHPStan\Rules\Functions\ArrayFilterRule: - phpstan.rules.rule: %featureToggles.arrayFilter% - PHPStan\Rules\Functions\ArrayValuesRule: - phpstan.rules.rule: %featureToggles.arrayValues% - PHPStan\Rules\Functions\CallUserFuncRule: - phpstan.rules.rule: %featureToggles.callUserFunc% - PHPStan\Rules\Functions\ParameterCastableToStringRule: - phpstan.rules.rule: %featureToggles.checkParameterCastableToStringFunctions% - PHPStan\Rules\Functions\ImplodeParameterCastableToStringRule: - phpstan.rules.rule: %featureToggles.checkParameterCastableToStringFunctions% - PHPStan\Rules\Functions\SortParameterCastableToStringRule: - phpstan.rules.rule: %featureToggles.checkParameterCastableToStringFunctions% + PHPStan\Rules\Functions\ParameterCastableToNumberRule: + phpstan.rules.rule: %featureToggles.checkParameterCastableToNumberFunctions% -rules: - - PHPStan\Rules\DateTimeInstantiationRule +autowiredAttributeServices: + # registers rules with #[RegisteredRule] attribute + level: 5 services: - - class: PHPStan\Rules\Functions\RandomIntParametersRule - arguments: - reportMaybes: %reportMaybes% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Functions\ArrayFilterRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - - - - class: PHPStan\Rules\Functions\ArrayValuesRule - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - - - - class: PHPStan\Rules\Functions\CallUserFuncRule - - - class: PHPStan\Rules\Functions\ImplodeFunctionRule - arguments: - disabled: %featureToggles.checkParameterCastableToStringFunctions% - tags: - - phpstan.rules.rule - - - class: PHPStan\Rules\Functions\ParameterCastableToStringRule - - - class: PHPStan\Rules\Functions\ImplodeParameterCastableToStringRule - - - class: PHPStan\Rules\Functions\SortParameterCastableToStringRule + class: PHPStan\Rules\Functions\ParameterCastableToNumberRule diff --git a/conf/config.level6.neon b/conf/config.level6.neon index 545fac6ad2..9da68b2d78 100644 --- a/conf/config.level6.neon +++ b/conf/config.level6.neon @@ -2,28 +2,9 @@ includes: - config.level5.neon parameters: - checkGenericClassInNonGenericObjectType: true - checkMissingIterableValueType: true checkMissingVarTagTypehint: true checkMissingTypehints: true -rules: - - PHPStan\Rules\Constants\MissingClassConstantTypehintRule - - PHPStan\Rules\Functions\MissingFunctionReturnTypehintRule - - PHPStan\Rules\Methods\MissingMethodReturnTypehintRule - - PHPStan\Rules\Properties\MissingPropertyTypehintRule - -services: - - - class: PHPStan\Rules\Functions\MissingFunctionParameterTypehintRule - arguments: - paramOut: %featureToggles.paramOutType% - tags: - - phpstan.rules.rule - - - - class: PHPStan\Rules\Methods\MissingMethodParameterTypehintRule - arguments: - paramOut: %featureToggles.paramOutType% - tags: - - phpstan.rules.rule +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 96e073cffb..664145f443 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1,14 +1,15 @@ includes: - parametersSchema.neon + - services.neon + - parsers.neon parameters: bootstrapFiles: - ../stubs/runtime/ReflectionUnionType.php - ../stubs/runtime/ReflectionAttribute.php - - ../stubs/runtime/Attribute.php + - ../stubs/runtime/Attribute85.php - ../stubs/runtime/ReflectionIntersectionType.php - excludes_analyse: [] - excludePaths: null + excludePaths: [] level: null paths: [] exceptions: @@ -20,97 +21,26 @@ parameters: checkedExceptionClasses: [] check: missingCheckedExceptionInThrows: false - tooWideThrowType: false + tooWideThrowType: true featureToggles: bleedingEdge: false - disableRuntimeReflectionProvider: true - skipCheckGenericClasses: - - DatePeriod - - CallbackFilterIterator - - FilterIterator - - RecursiveCallbackFilterIterator - - AppendIterator - - NoRewindIterator - - LimitIterator - - InfiniteIterator - - CachingIterator - - RegexIterator - - ReflectionEnum - explicitMixedInUnknownGenericNew: false - explicitMixedForGlobalVariables: false - explicitMixedViaIsArray: false - arrayFilter: false - arrayUnpacking: false - arrayValues: false - nodeConnectingVisitorCompatibility: true - nodeConnectingVisitorRule: false - illegalConstructorMethodCall: false - disableCheckMissingIterableValueType: false - strictUnnecessaryNullsafePropertyFetch: false - looseComparison: false - consistentConstructor: false - checkUnresolvableParameterTypes: false - readOnlyByPhpDoc: false - phpDocParserRequireWhitespaceBeforeDescription: false - phpDocParserIncludeLines: false - enableIgnoreErrorsWithinPhpDocs: false - runtimeReflectionRules: false - notAnalysedTrait: false - curlSetOptTypes: false - listType: false - abstractTraitMethod: false - missingMagicSerializationRule: false - nullContextForVoidReturningFunctions: false - unescapeStrings: false - alwaysCheckTooWideReturnTypeFinalMethods: false - duplicateStubs: false - logicalXor: false - betterNoop: false - invarianceComposition: false - alwaysTrueAlwaysReported: false - disableUnreachableBranchesRules: false - varTagType: false - closureDefaultParameterTypeRule: false - newRuleLevelHelper: false - instanceofType: false - paramOutVariance: false - allInvalidPhpDocs: false - strictStaticMethodTemplateTypeVariance: false - propertyVariance: false - genericPrototypeMessage: false + checkNonStringableDynamicAccess: false + checkParameterCastableToNumberFunctions: false + skipCheckGenericClasses: [] stricterFunctionMap: false - invalidPhpDocTagLine: false - detectDeadTypeInMultiCatch: false - zeroFiles: false - projectServicesNotInAnalysedPaths: false - callUserFunc: false - finalByPhpDoc: false - magicConstantOutOfContext: false - paramOutType: false - pure: false - checkParameterCastableToStringFunctions: false - narrowPregMatches: false - uselessReturnValue: false - printfArrayParameters: false - preciseMissingReturn: false - validatePregQuote: false - noImplicitWildcard: false + reportPreciseLineForUnusedFunctionParameter: false + internalTag: false + newStaticInAbstractClassStaticMethod: false fileExtensions: - php checkAdvancedIsset: false - checkAlwaysTrueCheckTypeFunctionCall: %featureToggles.alwaysTrueAlwaysReported% - checkAlwaysTrueInstanceof: %featureToggles.alwaysTrueAlwaysReported% - checkAlwaysTrueStrictComparison: %featureToggles.alwaysTrueAlwaysReported% - checkAlwaysTrueLooseComparison: %featureToggles.alwaysTrueAlwaysReported% reportAlwaysTrueInLastCondition: false checkClassCaseSensitivity: false checkExplicitMixed: false checkImplicitMixed: false checkFunctionArgumentTypes: false checkFunctionNameCase: false - checkGenericClassInNonGenericObjectType: false checkInternalClassCaseSensitivity: false - checkMissingIterableValueType: false checkMissingCallableSignature: false checkMissingVarTagTypehint: false checkArgumentsPassedByReference: false @@ -127,6 +57,7 @@ parameters: checkTooWideReturnTypesInProtectedAndPublicMethods: false checkUninitializedProperties: false checkDynamicProperties: false + strictRulesInstalled: false deprecationRulesInstalled: false inferPrivatePropertyTypeFromConstructor: false reportMaybes: false @@ -150,29 +81,29 @@ parameters: phpVersion: null polluteScopeWithLoopInitialAssignments: true polluteScopeWithAlwaysIterableForeach: true + polluteScopeWithBlock: true propertyAlwaysWrittenTags: [] propertyAlwaysReadTags: [] - fixerTmpDir: %pro.tmpDir% #unused 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: 256 reportUnmatchedIgnoredErrors: true - scopeClass: PHPStan\Analyser\MutatingScope typeAliases: [] universalObjectCratesClasses: - stdClass stubFiles: - ../stubs/ReflectionAttribute.stub - - ../stubs/ReflectionClass.stub - ../stubs/ReflectionClassConstant.stub - ../stubs/ReflectionFunctionAbstract.stub - ../stubs/ReflectionMethod.stub @@ -195,13 +126,12 @@ parameters: - ../stubs/arrayFunctions.stub - ../stubs/core.stub - ../stubs/typeCheckingFunctions.stub + - ../stubs/Countable.stub earlyTerminatingMethodCalls: [] earlyTerminatingFunctionCalls: [] - memoryLimitFile: %tmpDir%/.memory_limit - tempResultCachePath: %tmpDir%/resultCaches resultCachePath: %tmpDir%/resultCache.php + resultCacheSkipIfOlderThanDays: 7 resultCacheChecksProjectExtensionFilesDependencies: false - staticReflectionClassNamePatterns: [] dynamicConstantNames: - ICONV_IMPL - LIBXML_VERSION @@ -254,1946 +184,69 @@ 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 -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% - PhpParser\NodeVisitor\NodeConnectingVisitor: - phpstan.parser.richParserNodeVisitor: %featureToggles.nodeConnectingVisitorCompatibility% - PHPStan\Parser\CurlSetOptArgVisitor: - phpstan.parser.richParserNodeVisitor: %featureToggles.curlSetOptTypes% - PHPStan\Parser\TypeTraverserInstanceofVisitor: - phpstan.parser.richParserNodeVisitor: %featureToggles.instanceofType% - PHPStan\Type\Php\PregMatchTypeSpecifyingExtension: - phpstan.typeSpecifier.functionTypeSpecifyingExtension: %featureToggles.narrowPregMatches% - PHPStan\Type\Php\PregMatchParameterOutTypeExtension: - phpstan.functionParameterOutTypeExtension: %featureToggles.narrowPregMatches% - PHPStan\Type\Php\PregReplaceCallbackClosureTypeExtension: - phpstan.functionParameterClosureTypeExtension: %featureToggles.narrowPregMatches% + 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 - arguments: - options: - preserveOriginalNames: true - - - - class: PHPStan\Parser\AnonymousClassVisitor - tags: - - phpstan.parser.richParserNodeVisitor - - - - class: PHPStan\Parser\ArrayFilterArgVisitor - tags: - - phpstan.parser.richParserNodeVisitor - - - - class: PHPStan\Parser\ArrayMapArgVisitor - tags: - - phpstan.parser.richParserNodeVisitor - - - - class: PHPStan\Parser\ArrayWalkArgVisitor - tags: - - phpstan.parser.richParserNodeVisitor - - - - class: PHPStan\Parser\ClosureArgVisitor - tags: - - phpstan.parser.richParserNodeVisitor - - - - class: PHPStan\Parser\ClosureBindToVarVisitor - tags: - - phpstan.parser.richParserNodeVisitor - - - - class: PHPStan\Parser\ClosureBindArgVisitor - tags: - - phpstan.parser.richParserNodeVisitor - - - - class: PHPStan\Parser\CurlSetOptArgVisitor - - - - class: PHPStan\Parser\TypeTraverserInstanceofVisitor - - - - class: PHPStan\Parser\ArrowFunctionArgVisitor - tags: - - phpstan.parser.richParserNodeVisitor - - - - class: PHPStan\Parser\MagicConstantParamDefaultVisitor - tags: - - phpstan.parser.richParserNodeVisitor - - - - class: PHPStan\Parser\NewAssignedToPropertyVisitor - tags: - - phpstan.parser.richParserNodeVisitor - - - - class: PHPStan\Parser\ParentStmtTypesVisitor - tags: - - phpstan.parser.richParserNodeVisitor - - - - class: PHPStan\Parser\TryCatchTypeVisitor - tags: - - phpstan.parser.richParserNodeVisitor - - - - class: PHPStan\Parser\LastConditionVisitor - tags: - - phpstan.parser.richParserNodeVisitor - - - - class: PhpParser\NodeVisitor\NodeConnectingVisitor - - - - class: PHPStan\Node\Printer\ExprPrinter - - - - class: PHPStan\Node\Printer\Printer - - - - 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 - arguments: - quoteAwareConstExprString: %featureToggles.unescapeStrings% - - - - class: PHPStan\PhpDocParser\Parser\ConstExprParser - factory: @PHPStan\PhpDoc\ConstExprParserFactory::create() - - - - class: PHPStan\PhpDocParser\Parser\PhpDocParser - arguments: - requireWhitespaceBeforeDescription: %featureToggles.phpDocParserRequireWhitespaceBeforeDescription% - preserveTypeAliasesWithInvalidTypes: true - usedAttributes: - lines: %featureToggles.phpDocParserIncludeLines% - - - - class: PHPStan\PhpDoc\ConstExprParserFactory - arguments: - unescapeStrings: %featureToggles.unescapeStrings% - - - - 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 - arguments: - duplicateStubs: %featureToggles.duplicateStubs% - - - - class: PHPStan\PhpDoc\CountableStubFilesExtension - arguments: - bleedingEdge: %featureToggles.bleedingEdge% - tags: - - phpstan.stubFilesExtension - - - - class: PHPStan\PhpDoc\SocketSelectStubFilesExtension - tags: - - phpstan.stubFilesExtension - - - - class: PHPStan\PhpDoc\DefaultStubFilesProvider - arguments: - stubFiles: %stubFiles% - composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% - autowired: - - PHPStan\PhpDoc\StubFilesProvider - - - - class: PHPStan\PhpDoc\JsonValidateStubFilesExtension - tags: - - phpstan.stubFilesExtension - - - - class: PHPStan\PhpDoc\ReflectionEnumStubFilesExtension - tags: - - phpstan.stubFilesExtension - - - - class: PHPStan\Analyser\Analyser - arguments: - internalErrorsCountLimit: %internalErrorsCountLimit% - - - - class: PHPStan\Analyser\AnalyserResultFinalizer - arguments: - reportUnmatchedIgnoredErrors: %reportUnmatchedIgnoredErrors% - - - - class: PHPStan\Analyser\FileAnalyser - arguments: - parser: @defaultAnalysisParser - - - - class: PHPStan\Analyser\LocalIgnoresProcessor - - - - class: PHPStan\Analyser\RuleErrorTransformer - - - - class: PHPStan\Analyser\Ignore\IgnoredErrorHelper - arguments: - ignoreErrors: %ignoreErrors% - reportUnmatchedIgnoredErrors: %reportUnmatchedIgnoredErrors% - - - - class: PHPStan\Analyser\Ignore\IgnoreLexer - - - - class: PHPStan\Analyser\LazyInternalScopeFactory - arguments: - scopeClass: %scopeClass% - autowired: - - PHPStan\Analyser\InternalScopeFactory - - - - class: PHPStan\Analyser\ScopeFactory - - - - class: PHPStan\Analyser\NodeScopeResolver - arguments: - parser: @defaultAnalysisParser - reflector: @nodeScopeResolverReflector - polluteScopeWithLoopInitialAssignments: %polluteScopeWithLoopInitialAssignments% - polluteScopeWithAlwaysIterableForeach: %polluteScopeWithAlwaysIterableForeach% - earlyTerminatingMethodCalls: %earlyTerminatingMethodCalls% - earlyTerminatingFunctionCalls: %earlyTerminatingFunctionCalls% - implicitThrows: %exceptions.implicitThrows% - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - detectDeadTypeInMultiCatch: %featureToggles.detectDeadTypeInMultiCatch% - universalObjectCratesClasses: %universalObjectCratesClasses% - paramOutType: %featureToggles.paramOutType% - preciseMissingReturn: %featureToggles.preciseMissingReturn% - - - - class: PHPStan\Analyser\ConstantResolver - factory: @PHPStan\Analyser\ConstantResolverFactory::create() - - - - class: PHPStan\Analyser\ConstantResolverFactory - - - - implement: PHPStan\Analyser\ResultCache\ResultCacheManagerFactory - arguments: - scanFileFinder: @fileFinderScan - cacheFilePath: %resultCachePath% - analysedPaths: %analysedPaths% - composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% - usedLevel: %usedLevel% - cliAutoloadFile: %cliAutoloadFile% - bootstrapFiles: %bootstrapFiles% - scanFiles: %scanFiles% - scanDirectories: %scanDirectories% - checkDependenciesOfProjectExtensionFiles: %resultCacheChecksProjectExtensionFilesDependencies% - - - - class: PHPStan\Analyser\ResultCache\ResultCacheClearer - arguments: - cacheFilePath: %resultCachePath% - - - - class: PHPStan\Cache\Cache - arguments: - storage: @cacheStorage - - - - class: PHPStan\Collectors\Registry - factory: @PHPStan\Collectors\RegistryFactory::create - - - - class: PHPStan\Collectors\RegistryFactory - - - - class: PHPStan\Command\AnalyseApplication - - - - class: PHPStan\Command\AnalyserRunner - - - - class: PHPStan\Command\FixerApplication - arguments: - analysedPaths: %analysedPaths% - currentWorkingDirectory: %currentWorkingDirectory% - proTmpDir: %pro.tmpDir% - dnsServers: %pro.dnsServers% - composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% - allConfigFiles: %allConfigFiles% - cliAutoloadFile: %cliAutoloadFile% - bootstrapFiles: %bootstrapFiles% - editorUrl: %editorUrl% - usedLevel: %usedLevel% - - - - class: PHPStan\Dependency\DependencyResolver - - - - class: PHPStan\Dependency\ExportedNodeFetcher - arguments: - parser: @defaultAnalysisParser - - - - 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% - cliAutoloadFile: %cliAutoloadFile% - - - - class: PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider - factory: PHPStan\DependencyInjection\Reflection\LazyClassReflectionExtensionRegistryProvider - - - - class: PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider - factory: PHPStan\DependencyInjection\Type\LazyDynamicReturnTypeExtensionRegistryProvider - - - - class: PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider - factory: PHPStan\DependencyInjection\Type\LazyParameterOutTypeExtensionProvider - - - - class: PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider - factory: PHPStan\DependencyInjection\Type\LazyExpressionTypeResolverExtensionRegistryProvider - - - - class: PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider - factory: PHPStan\DependencyInjection\Type\LazyOperatorTypeSpecifyingExtensionRegistryProvider - - - - class: PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider - factory: PHPStan\DependencyInjection\Type\LazyDynamicThrowTypeExtensionProvider - - - - class: PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider - factory: PHPStan\DependencyInjection\Type\LazyParameterClosureTypeExtensionProvider - - - - class: PHPStan\File\FileHelper - arguments: - workingDirectory: %currentWorkingDirectory% - - - - class: PHPStan\File\FileExcluderFactory - arguments: - obsoleteExcludesAnalyse: %excludes_analyse% - excludePaths: %excludePaths% - - - - implement: PHPStan\File\FileExcluderRawFactory - arguments: - noImplicitWildcard: %featureToggles.noImplicitWildcard% - - fileExcluderAnalyse: - class: PHPStan\File\FileExcluder - factory: @PHPStan\File\FileExcluderFactory::createAnalyseFileExcluder() - autowired: false - - fileExcluderScan: - class: PHPStan\File\FileExcluder - factory: @PHPStan\File\FileExcluderFactory::createScanFileExcluder() - autowired: false - - fileFinderAnalyse: - class: PHPStan\File\FileFinder - arguments: - fileExcluder: @fileExcluderAnalyse - fileExtensions: %fileExtensions% - autowired: false - - fileFinderScan: - class: PHPStan\File\FileFinder - arguments: - fileExcluder: @fileExcluderScan - fileExtensions: %fileExtensions% - autowired: false - - - - class: PHPStan\File\FileMonitor - arguments: - fileFinder: @fileFinderAnalyse - - - - class: PHPStan\Parser\DeclarePositionVisitor - tags: - - phpstan.parser.richParserNodeVisitor - - - - class: PHPStan\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% - tags: - - phpstan.diagnoseExtension - - - - class: PHPStan\Parser\FunctionCallStatementFinder - - - - class: PHPStan\Process\CpuCoreCounter - - - - implement: PHPStan\Reflection\FunctionReflectionFactory - arguments: - parser: @defaultAnalysisParser - - - - class: PHPStan\Reflection\InitializerExprTypeResolver - arguments: - usePathConstantsAsConstantString: %usePathConstantsAsConstantString% - - - - class: PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension - - - - class: PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension - - - - class: PHPStan\Reflection\BetterReflection\SourceLocator\CachingVisitor - - - - class: PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher - arguments: - parser: @defaultAnalysisParser - - - - 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\RequireExtension\RequireExtendsMethodsClassReflectionExtension - - - - class: PHPStan\Reflection\RequireExtension\RequireExtendsPropertiesClassReflectionExtension - - - - 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: - parser: @defaultAnalysisParser - inferPrivatePropertyTypeFromConstructor: %inferPrivatePropertyTypeFromConstructor% - - - - implement: PHPStan\Reflection\Php\PhpMethodReflectionFactory - arguments: - parser: @defaultAnalysisParser - - - - class: PHPStan\Reflection\Php\Soap\SoapClientMethodsClassReflectionExtension - tags: - - phpstan.broker.methodsClassReflectionExtension - - - - class: PHPStan\Reflection\Php\EnumAllowedSubTypesClassReflectionExtension - tags: - - phpstan.broker.allowedSubTypesClassReflectionExtension - - - - class: PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension - tags: - - phpstan.broker.propertiesClassReflectionExtension - arguments: - classes: %universalObjectCratesClasses% - - - - class: PHPStan\Reflection\PHPStan\NativeReflectionEnumReturnDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - arguments: - className: PHPStan\Reflection\ClassReflection - methodName: getNativeReflection - - - - class: PHPStan\Reflection\PHPStan\NativeReflectionEnumReturnDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - arguments: - className: PHPStan\Reflection\Php\BuiltinMethodReflection - methodName: getDeclaringClass - - - - class: PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider - factory: PHPStan\Reflection\ReflectionProvider\LazyReflectionProviderProvider - - - - class: PHPStan\Reflection\SignatureMap\NativeFunctionReflectionProvider - arguments: - reflector: @betterReflectionReflector - - - - class: PHPStan\Reflection\SignatureMap\SignatureMapParser - - - - class: PHPStan\Reflection\SignatureMap\FunctionSignatureMapProvider - arguments: - stricterFunctionMap: %featureToggles.stricterFunctionMap% - 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 - arguments: - deprecationRulesInstalled: %deprecationRulesInstalled% - - - - class: PHPStan\Rules\Arrays\NonexistentOffsetInArrayDimFetchCheck - arguments: - reportMaybes: %reportMaybes% - bleedingEdge: %featureToggles.bleedingEdge% - reportPossiblyNonexistentGeneralArrayOffset: %reportPossiblyNonexistentGeneralArrayOffset% - reportPossiblyNonexistentConstantArrayOffset: %reportPossiblyNonexistentConstantArrayOffset% - - - - class: PHPStan\Rules\ClassNameCheck - - - - class: PHPStan\Rules\ClassCaseSensitivityCheck - arguments: - checkInternalClassCaseSensitivity: %checkInternalClassCaseSensitivity% - - - - class: PHPStan\Rules\ClassForbiddenNameCheck - - - - class: PHPStan\Rules\Classes\LocalTypeAliasesCheck - arguments: - globalTypeAliases: %typeAliases% - - - - class: PHPStan\Rules\Comparison\ConstantConditionRuleHelper - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - looseComparisonRuleEnabled: %featureToggles.looseComparison% + class: PHPStan\Rules\Exceptions\MissingCheckedExceptionInFunctionThrowsRule - - class: PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper - arguments: - universalObjectCratesClasses: %universalObjectCratesClasses% - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - nullContextForVoidReturningFunctions: %featureToggles.nullContextForVoidReturningFunctions% + class: PHPStan\Rules\Exceptions\MissingCheckedExceptionInMethodThrowsRule - - 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\MissingCheckedExceptionInPropertyHookThrowsRule - - class: PHPStan\Rules\Exceptions\MissingCheckedExceptionInFunctionThrowsRule - - - - 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% - checkUnresolvableParameterTypes: %featureToggles.checkUnresolvableParameterTypes% - - - - class: PHPStan\Rules\FunctionDefinitionCheck - arguments: - checkClassCaseSensitivity: %checkClassCaseSensitivity% - checkThisOnly: %checkThisOnly% - - - - class: PHPStan\Rules\FunctionReturnTypeCheck - - - class: PHPStan\Rules\ParameterCastableToStringCheck - - - - class: PHPStan\Rules\Generics\CrossCheckInterfacesHelper - - - - class: PHPStan\Rules\Generics\GenericAncestorsCheck - arguments: - checkGenericClassInNonGenericObjectType: %checkGenericClassInNonGenericObjectType% - skipCheckGenericClasses: %featureToggles.skipCheckGenericClasses% - - - - class: PHPStan\Rules\Generics\GenericObjectTypeCheck - - - - class: PHPStan\Rules\Generics\TemplateTypeCheck - arguments: - checkClassCaseSensitivity: %checkClassCaseSensitivity% - - - - class: PHPStan\Rules\Generics\VarianceCheck - arguments: - checkParamOutVariance: %featureToggles.paramOutVariance% - strictStaticVariance: %featureToggles.strictStaticMethodTemplateTypeVariance% - - - - class: PHPStan\Rules\IssetCheck - arguments: - checkAdvancedIsset: %checkAdvancedIsset% - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - strictUnnecessaryNullsafePropertyFetch: %featureToggles.strictUnnecessaryNullsafePropertyFetch% - - - - class: PHPStan\Rules\Methods\MethodCallCheck - arguments: - checkFunctionNameCase: %checkFunctionNameCase% - reportMagicMethods: %reportMagicMethods% - - - - class: PHPStan\Rules\Methods\StaticMethodCallCheck - arguments: - checkFunctionNameCase: %checkFunctionNameCase% - reportMagicMethods: %reportMagicMethods% - - - - # checked as part of OverridingMethodRule - class: PHPStan\Rules\Methods\MethodSignatureRule - arguments: - reportMaybes: %reportMaybesInMethodSignatures% - reportStatic: %reportStaticMethodSignatures% - abstractTraitMethod: %featureToggles.abstractTraitMethod% - - - - class: PHPStan\Rules\Methods\MethodParameterComparisonHelper - arguments: - genericPrototypeMessage: %featureToggles.genericPrototypeMessage% - - - - class: PHPStan\Rules\MissingTypehintCheck - arguments: - checkMissingIterableValueType: %checkMissingIterableValueType% - disableCheckMissingIterableValueType: %featureToggles.disableCheckMissingIterableValueType% - checkGenericClassInNonGenericObjectType: %checkGenericClassInNonGenericObjectType% - checkMissingCallableSignature: %checkMissingCallableSignature% - skipCheckGenericClasses: %featureToggles.skipCheckGenericClasses% - - - - class: PHPStan\Rules\NullsafeCheck - - - - class: PHPStan\Rules\Constants\LazyAlwaysUsedClassConstantsExtensionProvider - - - - class: PHPStan\Rules\Methods\LazyAlwaysUsedMethodExtensionProvider - - - - class: PHPStan\Rules\PhpDoc\ConditionalReturnTypeRuleHelper - - - - class: PHPStan\Rules\PhpDoc\AssertRuleHelper - - - - class: PHPStan\Rules\PhpDoc\UnresolvableTypeHelper - - - - class: PHPStan\Rules\PhpDoc\GenericCallableRuleHelper - - - - class: PHPStan\Rules\PhpDoc\VarTagTypeRuleHelper - arguments: - checkTypeAgainstPhpDocType: %reportWrongPhpDocTypeInVarTag% - strictWideningCheck: %reportAnyTypeWideningInVarTag% - - - - class: PHPStan\Rules\Playground\NeverRuleHelper - - - - class: PHPStan\Rules\Properties\LazyReadWritePropertiesExtensionProvider - - - - class: PHPStan\Rules\Properties\PropertyDescriptor - - - - class: PHPStan\Rules\Properties\PropertyReflectionFinder - - - - class: PHPStan\Rules\Pure\FunctionPurityCheck - - - - class: PHPStan\Rules\RuleLevelHelper - arguments: - checkNullables: %checkNullables% - checkThisOnly: %checkThisOnly% - checkUnionTypes: %checkUnionTypes% - checkExplicitMixed: %checkExplicitMixed% - checkImplicitMixed: %checkImplicitMixed% - newRuleLevelHelper: %featureToggles.newRuleLevelHelper% - checkBenevolentUnionTypes: %checkBenevolentUnionTypes% - - - - class: PHPStan\Rules\UnusedFunctionParametersCheck - - - - class: PHPStan\Rules\TooWideTypehints\TooWideParameterOutTypeCheck - - - - class: PHPStan\Type\FileTypeMapper - arguments: - phpParser: @defaultAnalysisParser - - - - class: PHPStan\Type\TypeAliasResolver - factory: PHPStan\Type\UsefulTypeAliasResolver - arguments: - globalTypeAliases: %typeAliases% - - - - class: PHPStan\Type\TypeAliasResolverProvider - factory: PHPStan\Type\LazyTypeAliasResolverProvider - - - - class: PHPStan\Type\BitwiseFlagHelper - - - - class: PHPStan\Type\Php\AbsFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArgumentBasedFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArrayIntersectKeyFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArrayChunkFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArrayColumnFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArrayCombineFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArrayCurrentDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArrayFillFunctionReturnTypeExtension - tags: - - 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\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\ArrayReplaceFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArrayReverseFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArrayShiftFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArraySliceFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArraySpliceFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArraySearchFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArraySearchFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\ArrayValuesFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ArraySumFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\AssertThrowTypeExtension - tags: - - phpstan.dynamicFunctionThrowTypeExtension - - - - class: PHPStan\Type\Php\BackedEnumFromMethodDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicStaticMethodReturnTypeExtension - - - - class: PHPStan\Type\Php\Base64DecodeDynamicFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\BcMathStringOrNullReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ClosureBindDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicStaticMethodReturnTypeExtension - - - - class: PHPStan\Type\Php\ClosureBindToDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - - class: PHPStan\Type\Php\ClosureFromCallableDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicStaticMethodReturnTypeExtension - - - - class: PHPStan\Type\Php\CompactFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - arguments: - checkMaybeUndefinedVariables: %checkMaybeUndefinedVariables% - - - - class: PHPStan\Type\Php\ConstantFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ConstantHelper - - - - class: PHPStan\Type\Php\CountFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\CountFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\CurlGetinfoFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\CurlInitReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\DateFunctionReturnTypeHelper - - - - class: PHPStan\Type\Php\DateFormatFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\DateFormatMethodReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - - class: PHPStan\Type\Php\DateFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\DateIntervalConstructorThrowTypeExtension - tags: - - phpstan.dynamicStaticMethodThrowTypeExtension - - - - class: PHPStan\Type\Php\DateIntervalDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicStaticMethodReturnTypeExtension - - - - class: PHPStan\Type\Php\DateTimeCreateDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\DateTimeDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\DateTimeModifyReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - arguments: - dateTimeClass: DateTime - - - - class: PHPStan\Type\Php\DateTimeModifyReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - arguments: - dateTimeClass: DateTimeImmutable - - - - class: PHPStan\Type\Php\DateTimeConstructorThrowTypeExtension - tags: - - phpstan.dynamicStaticMethodThrowTypeExtension - - - - class: PHPStan\Type\Php\DateTimeModifyMethodThrowTypeExtension - tags: - - phpstan.dynamicMethodThrowTypeExtension - - - - class: PHPStan\Type\Php\DateTimeZoneConstructorThrowTypeExtension - tags: - - phpstan.dynamicStaticMethodThrowTypeExtension - - - - class: PHPStan\Type\Php\DsMapDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - - class: PHPStan\Type\Php\DsMapDynamicMethodThrowTypeExtension - tags: - - phpstan.dynamicMethodThrowTypeExtension - - - - class: PHPStan\Type\Php\DioStatDynamicFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ExplodeFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\FilterFunctionReturnTypeHelper - - - - class: PHPStan\Type\Php\FilterInputDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\FilterVarDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\FilterVarArrayDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\GetCalledClassDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\GetClassDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\GetDebugTypeFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\GetParentClassDynamicFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\GettypeFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\GettimeofdayDynamicFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - class: PHPStan\Type\Php\HashFunctionsReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\IntdivThrowTypeExtension - tags: - - phpstan.dynamicFunctionThrowTypeExtension - - - - class: PHPStan\Type\Php\IniGetReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\JsonThrowTypeExtension - tags: - - phpstan.dynamicFunctionThrowTypeExtension - - - - class: PHPStan\Type\Php\PregMatchTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\PregMatchParameterOutTypeExtension - - - - class: PHPStan\Type\Php\PregReplaceCallbackClosureTypeExtension - - - - class: PHPStan\Type\Php\RegexArrayShapeMatcher - - - - class: PHPStan\Type\Regex\RegexGroupParser - - - - class: PHPStan\Type\Regex\RegexExpressionHelper - - - - class: PHPStan\Type\Php\ReflectionClassConstructorThrowTypeExtension - tags: - - phpstan.dynamicStaticMethodThrowTypeExtension - - - - class: PHPStan\Type\Php\ReflectionFunctionConstructorThrowTypeExtension - tags: - - phpstan.dynamicStaticMethodThrowTypeExtension - - - - class: PHPStan\Type\Php\ReflectionMethodConstructorThrowTypeExtension - tags: - - phpstan.dynamicStaticMethodThrowTypeExtension - - - - class: PHPStan\Type\Php\ReflectionPropertyConstructorThrowTypeExtension - tags: - - phpstan.dynamicStaticMethodThrowTypeExtension - - - - class: PHPStan\Type\Php\StrContainingTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - 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\PregFilterFunctionReturnTypeExtension - 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\LtrimFunctionReturnTypeExtension - 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\MbStrlenFunctionReturnTypeExtension - 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\SetTypeFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\StrCaseFunctionsReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\StrlenFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\StrIncrementDecrementFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\StrPadFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\StrRepeatFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\SubstrDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\ThrowableReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - - class: PHPStan\Type\Php\ParseUrlFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\TriggerErrorDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\VersionCompareFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\PowFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\RoundFunctionReturnTypeExtension - 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\ClassImplementsFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\DefineConstantTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\DefinedConstantTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\FunctionExistsFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\InArrayFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsArrayFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - arguments: - explicitMixed: %featureToggles.explicitMixedViaIsArray% - - - - class: PHPStan\Type\Php\IsCallableFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsIterableFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsSubclassOfFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IteratorToArrayFunctionReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\IsAFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsAFunctionTypeSpecifyingHelper - - - - class: PHPStan\Type\Php\CtypeDigitFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\JsonThrowOnErrorDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - - - class: PHPStan\Type\Php\TypeSpecifyingFunctionsDynamicReturnTypeExtension - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - universalObjectCratesClasses: %universalObjectCratesClasses% - nullContextForVoidReturningFunctions: %featureToggles.nullContextForVoidReturningFunctions% - 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\SscanfFunctionDynamicReturnTypeExtension - 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 - - - - class: PHPStan\Type\Php\DatePeriodConstructorReturnTypeExtension - tags: - - phpstan.broker.dynamicStaticMethodReturnTypeExtension - - - - class: PHPStan\Type\ClosureTypeFactory - arguments: - parser: @currentPhpVersionPhpParser - - - - class: PHPStan\Type\Constant\OversizedArrayBuilder - - - - class: PHPStan\Rules\Functions\PrintfHelper - - exceptionTypeResolver: - class: PHPStan\Rules\Exceptions\ExceptionTypeResolver - factory: @PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver - - typeSpecifier: - class: PHPStan\Analyser\TypeSpecifier - 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 - lexer: @currentPhpVersionLexer - enableIgnoreErrorsWithinPhpDocs: %featureToggles.enableIgnoreErrorsWithinPhpDocs% - autowired: no - - currentPhpVersionSimpleParser: - class: PHPStan\Parser\CleaningParser - arguments: - wrappedParser: @currentPhpVersionSimpleDirectParser - autowired: no - - currentPhpVersionSimpleDirectParser: - class: PHPStan\Parser\SimpleParser - arguments: - parser: @currentPhpVersionPhpParser - autowired: no - - defaultAnalysisParser: - class: PHPStan\Parser\CachedParser - arguments: - originalParser: @pathRoutingParser - cachedNodesByStringCountMax: %cache.nodesByStringCountMax% - autowired: false - - phpParserDecorator: - class: PHPStan\Parser\PhpParserDecorator - arguments: - wrappedParser: @defaultAnalysisParser - autowired: false - - 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\LazyRegistry - autowired: - - PHPStan\Rules\Registry - - stubPhpDocProvider: - class: PHPStan\PhpDoc\StubPhpDocProvider - arguments: - parser: @defaultAnalysisParser - - # Reflection providers - - reflectionProviderFactory: - class: PHPStan\Reflection\ReflectionProvider\ReflectionProviderFactory - arguments: - staticReflectionProvider: @betterReflectionProvider - - 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 - - originalBetterReflectionReflector: - class: PHPStan\BetterReflection\Reflector\DefaultReflector - arguments: - sourceLocator: @betterReflectionSourceLocator - - betterReflectionReflector: - class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingReflector - arguments: - reflector: @originalBetterReflectionReflector - autowired: false - - # deprecated - betterReflectionClassReflector: - class: PHPStan\BetterReflection\Reflector\ClassReflector - arguments: - sourceLocator: @betterReflectionSourceLocator - autowired: false - - # deprecated - betterReflectionFunctionReflector: - class: PHPStan\BetterReflection\Reflector\FunctionReflector - arguments: - sourceLocator: @betterReflectionSourceLocator - autowired: false - - # deprecated - betterReflectionConstantReflector: - class: PHPStan\BetterReflection\Reflector\ConstantReflector - arguments: - sourceLocator: @betterReflectionSourceLocator - autowired: false - - nodeScopeResolverReflector: - factory: @betterReflectionReflector - autowired: false - - betterReflectionProvider: - class: PHPStan\Reflection\BetterReflection\BetterReflectionProvider - arguments: - reflector: @betterReflectionReflector - universalObjectCratesClasses: %universalObjectCratesClasses% - autowired: false - - - - class: PHPStan\Reflection\BetterReflection\BetterReflectionSourceLocatorFactory - arguments: - parser: @phpParserDecorator - php8Parser: @php8PhpParser - scanFiles: %scanFiles% - scanDirectories: %scanDirectories% - analysedPaths: %analysedPaths% - composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% - analysedPathsFromConfig: %analysedPathsFromConfig% - - - - implement: PHPStan\Reflection\BetterReflection\BetterReflectionProviderFactory - arguments: - universalObjectCratesClasses: %universalObjectCratesClasses% - - - - class: PHPStan\Reflection\BetterReflection\SourceStubber\PhpStormStubsSourceStubberFactory - arguments: - phpParser: @php8PhpParser - - - - 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 - - - - class: PHPStan\Reflection\BetterReflection\SourceStubber\ReflectionSourceStubberFactory - - php8Lexer: - class: PhpParser\Lexer\Emulative - factory: @PHPStan\Parser\LexerFactory::createEmulative() - 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 - - phpstanDiagnoseExtension: - class: PHPStan\Diagnose\PHPStanDiagnoseExtension - arguments: - composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% - allConfigFiles: %allConfigFiles% - autowired: false - - # Error formatters - - - - class: PHPStan\Command\ErrorFormatter\CiDetectedErrorFormatter - autowired: - - PHPStan\Command\ErrorFormatter\CiDetectedErrorFormatter - - errorFormatter.raw: - class: PHPStan\Command\ErrorFormatter\RawErrorFormatter - - errorFormatter.table: - class: PHPStan\Command\ErrorFormatter\TableErrorFormatter - arguments: - simpleRelativePathHelper: @simpleRelativePathHelper - showTipsOfTheDay: %tipsOfTheDay% - editorUrl: %editorUrl% - editorUrlTitle: %editorUrlTitle% - - errorFormatter.checkstyle: - class: PHPStan\Command\ErrorFormatter\CheckstyleErrorFormatter - 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.stubValidator.neon b/conf/config.stubValidator.neon index 1645698a92..ad90340a64 100644 --- a/conf/config.stubValidator.neon +++ b/conf/config.stubValidator.neon @@ -1,8 +1,6 @@ parameters: checkThisOnly: false checkClassCaseSensitivity: true - checkGenericClassInNonGenericObjectType: true - checkMissingIterableValueType: true checkMissingTypehints: true checkMissingCallableSignature: false __validate: false @@ -13,9 +11,30 @@ services: arguments: php8Parser: @php8PhpParser - nodeScopeResolverClassReflector: + # overrides service from parsers.neon + defaultAnalysisParser!: + factory: @stubParser + + # 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 + + # overrides service from parsers.neon + currentPhpVersionPhpParser: + factory: @php8PhpParser + autowired: false + stubBetterReflectionProvider: class: PHPStan\Reflection\BetterReflection\BetterReflectionProvider arguments: @@ -33,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 index a217bfc4e0..76d8e86b40 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -1,7 +1,6 @@ parametersSchema: bootstrapFiles: listOf(string()) - excludes_analyse: listOf(string()) - excludePaths: schema(anyOf( + excludePaths: anyOf( structure([ analyse: listOf(string()), ]), @@ -12,7 +11,7 @@ parametersSchema: analyse: listOf(string()), analyseAndScan: listOf(string()) ]) - ), nullable()) + ) level: schema(anyOf(int(), string()), nullable()) paths: listOf(string()) exceptions: structure([ @@ -29,83 +28,23 @@ parametersSchema: ]) featureToggles: structure([ bleedingEdge: bool(), - disableRuntimeReflectionProvider: bool(), + checkNonStringableDynamicAccess: bool(), + checkParameterCastableToNumberFunctions: bool(), skipCheckGenericClasses: listOf(string()), - explicitMixedInUnknownGenericNew: bool(), - explicitMixedForGlobalVariables: bool(), - explicitMixedViaIsArray: bool(), - arrayFilter: bool(), - arrayUnpacking: bool(), - arrayValues: bool(), - nodeConnectingVisitorCompatibility: bool(), - nodeConnectingVisitorRule: bool(), - illegalConstructorMethodCall: bool(), - disableCheckMissingIterableValueType: bool(), - strictUnnecessaryNullsafePropertyFetch: bool(), - looseComparison: bool(), - consistentConstructor: bool() - checkUnresolvableParameterTypes: bool() - readOnlyByPhpDoc: bool() - phpDocParserRequireWhitespaceBeforeDescription: bool() - phpDocParserIncludeLines: bool() - enableIgnoreErrorsWithinPhpDocs: bool() - runtimeReflectionRules: bool() - notAnalysedTrait: bool() - curlSetOptTypes: bool() - listType: bool() - abstractTraitMethod: bool() - missingMagicSerializationRule: bool() - nullContextForVoidReturningFunctions: bool() - unescapeStrings: bool() - alwaysCheckTooWideReturnTypeFinalMethods: bool() - duplicateStubs: bool() - logicalXor: bool() - betterNoop: bool() - invarianceComposition: bool() - alwaysTrueAlwaysReported: bool() - disableUnreachableBranchesRules: bool() - varTagType: bool() - closureDefaultParameterTypeRule: bool() - newRuleLevelHelper: bool() - instanceofType: bool() - paramOutVariance: bool() - allInvalidPhpDocs: bool() - strictStaticMethodTemplateTypeVariance: bool() - propertyVariance: bool() - genericPrototypeMessage: bool() stricterFunctionMap: bool() - invalidPhpDocTagLine: bool() - detectDeadTypeInMultiCatch: bool() - zeroFiles: bool() - projectServicesNotInAnalysedPaths: bool() - callUserFunc: bool() - finalByPhpDoc: bool() - magicConstantOutOfContext: bool() - paramOutType: bool() - pure: bool() - checkParameterCastableToStringFunctions: bool() - narrowPregMatches: bool() - uselessReturnValue: bool() - printfArrayParameters: bool() - preciseMissingReturn: bool() - validatePregQuote: bool() - noImplicitWildcard: bool() + reportPreciseLineForUnusedFunctionParameter: bool() + internalTag: bool() + newStaticInAbstractClassStaticMethod: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() - checkAlwaysTrueCheckTypeFunctionCall: bool() - checkAlwaysTrueInstanceof: bool() - checkAlwaysTrueStrictComparison: bool() - checkAlwaysTrueLooseComparison: bool() reportAlwaysTrueInLastCondition: bool() checkClassCaseSensitivity: bool() checkExplicitMixed: bool() checkImplicitMixed: bool() checkFunctionArgumentTypes: bool() checkFunctionNameCase: bool() - checkGenericClassInNonGenericObjectType: bool() checkInternalClassCaseSensitivity: bool() - checkMissingIterableValueType: bool() checkMissingCallableSignature: bool() checkMissingVarTagTypehint: bool() checkArgumentsPassedByReference: bool() @@ -122,9 +61,14 @@ parametersSchema: checkTooWideReturnTypesInProtectedAndPublicMethods: bool() checkUninitializedProperties: bool() checkDynamicProperties: bool() + strictRulesInstalled: bool() deprecationRulesInstalled: bool() inferPrivatePropertyTypeFromConstructor: bool() + tips: structure([ + discoveringSymbols: bool() + treatPhpDocTypesAsCertain: bool() + ]) tipsOfTheDay: bool() reportMaybes: bool() reportMaybesInMethodSignatures: bool() @@ -142,9 +86,16 @@ parametersSchema: minimumNumberOfJobsPerProcess: int(), buffer: int() ]) - phpVersion: schema(anyOf(schema(int(), min(70100), max(80399))), nullable()) + 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()) @@ -159,12 +110,14 @@ parametersSchema: structure([ ?messages: listOf(string()) ?identifier: string() + ?identifiers: listOf(string()) ?path: string() ?reportUnmatched: bool() ]), structure([ ?message: string() ?identifier: string() + ?identifiers: listOf(string()) ?path: string() ?reportUnmatched: bool() ]), @@ -173,39 +126,38 @@ parametersSchema: count: int() path: string() ?identifier: string() + ?identifiers: listOf(string()) ?reportUnmatched: bool() ]), structure([ ?message: string() paths: listOf(string()) ?identifier: string() + ?identifiers: listOf(string()) ?reportUnmatched: bool() ]), structure([ ?messages: listOf(string()) paths: listOf(string()) ?identifier: string() + ?identifiers: listOf(string()) ?reportUnmatched: bool() ]) ) ) internalErrorsCountLimit: int() cache: structure([ - nodesByFileCountMax: int() nodesByStringCountMax: int() ]) reportUnmatchedIgnoredErrors: bool() - scopeClass: string() typeAliases: arrayOf(string()) universalObjectCratesClasses: listOf(string()) stubFiles: listOf(string()) earlyTerminatingMethodCalls: arrayOf(listOf(string())) earlyTerminatingFunctionCalls: listOf(string()) - memoryLimitFile: string() - tempResultCachePath: string() resultCachePath: string() + resultCacheSkipIfOlderThanDays: int() resultCacheChecksProjectExtensionFilesDependencies: bool() - staticReflectionClassNamePatterns: listOf(string()) dynamicConstantNames: listOf(string()) customRulesetUsed: schema(bool(), nullable()) rootDir: string() @@ -215,7 +167,6 @@ parametersSchema: mixinExcludeClasses: listOf(string()) scanFiles: listOf(string()) scanDirectories: listOf(string()) - fixerTmpDir: string() #unused editorUrl: schema(string(), nullable()) editorUrlTitle: schema(string(), nullable()) errorFormat: schema(string(), nullable()) @@ -225,6 +176,13 @@ parametersSchema: ]) env: arrayOf(string(), anyOf(int(), string())) sysGetTempDir: string() + parametersNotInvalidatingCache: listOf(schema(anyOf( + string(), + listOf(string()), + ))) + + # playground mode + sourceLocatorPlaygroundMode: bool() # irrelevant Nette parameters debugMode: bool() @@ -233,7 +191,7 @@ parametersSchema: __validate: bool() # internal parameters only for DerivativeContainerFactory - additionalConfigFiles: arrayOf(string()) + additionalConfigFiles: listOf(string()) generateBaselineFile: schema(string(), nullable()) analysedPaths: listOf(string()) allConfigFiles: listOf(string()) @@ -241,3 +199,26 @@ parametersSchema: 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/e2e/bug-11819/phpstan.neon b/e2e/bug-11819/phpstan.neon new file mode 100644 index 0000000000..5fbd64483a --- /dev/null +++ b/e2e/bug-11819/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 5 + paths: + - test.php diff --git a/e2e/bug-11819/test.php b/e2e/bug-11819/test.php new file mode 100644 index 0000000000..267e1647ad --- /dev/null +++ b/e2e/bug-11819/test.php @@ -0,0 +1,11 @@ + + */ +class DummyRule implements Rule +{ + + public function getNodeType(): string + { + return InClassNode::class; + } + + /** + * @param InClassNode $node + * @return list + */ + public function processNode( + Node $node, + Scope $scope, + ): array + { + return [FatalErrorWhenAutoloaded::AUTOLOAD]; + } + +} diff --git a/e2e/bug-11826/src/FatalErrorWhenAutoloaded.php b/e2e/bug-11826/src/FatalErrorWhenAutoloaded.php new file mode 100644 index 0000000000..a75127a356 --- /dev/null +++ b/e2e/bug-11826/src/FatalErrorWhenAutoloaded.php @@ -0,0 +1,11 @@ +getName() === 'belongsTo'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { + $returnType = $methodReflection->getVariants()[0]->getReturnType(); + $argType = $scope->getType($methodCall->getArgs()[0]->value); + $modelClass = $argType->getClassStringObjectType()->getObjectClassNames()[0]; + + return new GenericObjectType($returnType->getObjectClassNames()[0], [ + new ObjectType($modelClass), + $scope->getType($methodCall->var), + ]); + } +} + diff --git a/e2e/bug-11857/src/test.php b/e2e/bug-11857/src/test.php new file mode 100644 index 0000000000..5c237f25e8 --- /dev/null +++ b/e2e/bug-11857/src/test.php @@ -0,0 +1,70 @@ + */ + public function belongsTo(string $related): BelongsTo + { + return new BelongsTo(); + } +} + +/** + * @template TRelatedModel of Model + * @template TDeclaringModel of Model + */ +class BelongsTo {} + +class User extends Model {} + +class Post extends Model +{ + /** @return BelongsTo */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** @return BelongsTo */ + public function userSelf(): BelongsTo + { + /** @phpstan-ignore return.type */ + return $this->belongsTo(User::class); + } +} + +class ChildPost extends Post {} + +final class Comment extends Model +{ + // This model is final, so either of these + // two methods would work. It seems that + // PHPStan is automatically converting the + // `$this` to a `self` type in the user docblock, + // but it is not doing so likewise for the `$this` + // that is returned by the dynamic return extension. + + /** @return BelongsTo */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** @return BelongsTo */ + public function user2(): BelongsTo + { + /** @phpstan-ignore return.type */ + return $this->belongsTo(User::class); + } +} + +function test(ChildPost $child): void +{ + assertType('Bug11857\BelongsTo', $child->user()); + // This demonstrates why `$this` is needed in non-final models + assertType('Bug11857\BelongsTo', $child->userSelf()); // should be: Bug11857\BelongsTo +} diff --git a/e2e/bug-12606/phpstan.neon b/e2e/bug-12606/phpstan.neon new file mode 100644 index 0000000000..1557144b26 --- /dev/null +++ b/e2e/bug-12606/phpstan.neon @@ -0,0 +1,2 @@ +includes: + - %env.CONFIGTEST%.neon diff --git a/e2e/bug-12606/src/empty.php b/e2e/bug-12606/src/empty.php new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/bug-12606/test.neon b/e2e/bug-12606/test.neon new file mode 100644 index 0000000000..c308dcf542 --- /dev/null +++ b/e2e/bug-12606/test.neon @@ -0,0 +1,4 @@ +parameters: + level: 8 + paths: + - src diff --git a/e2e/bug-12606/test.php b/e2e/bug-12606/test.php new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/bug-9622-trait/baseline-1.neon b/e2e/bug-9622-trait/baseline-1.neon index 1548dbca10..caa24d89e8 100644 --- a/e2e/bug-9622-trait/baseline-1.neon +++ b/e2e/bug-9622-trait/baseline-1.neon @@ -1,6 +1,6 @@ parameters: ignoreErrors: - - message: "#^Offset 'foo' does not exist on array\\{foo\\?\\: int\\}\\.$#" + message: "#^Offset 'foo' might not exist on array\\{foo\\?\\: int\\}\\.$#" count: 1 path: src/UsesBar.php diff --git a/e2e/bug-9622/baseline-1.neon b/e2e/bug-9622/baseline-1.neon index a7df787390..0ae88bf65b 100644 --- a/e2e/bug-9622/baseline-1.neon +++ b/e2e/bug-9622/baseline-1.neon @@ -1,6 +1,6 @@ parameters: ignoreErrors: - - message: "#^Offset 'foo' does not exist on array\\{foo\\?\\: int\\}\\.$#" + message: "#^Offset 'foo' might not exist on array\\{foo\\?\\: int\\}\\.$#" count: 1 path: src/Bar.php diff --git a/e2e/composer-and-phpstan-version-config/.gitignore b/e2e/composer-and-phpstan-version-config/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-and-phpstan-version-config/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-and-phpstan-version-config/composer.json b/e2e/composer-and-phpstan-version-config/composer.json new file mode 100644 index 0000000000..8fe9570814 --- /dev/null +++ b/e2e/composer-and-phpstan-version-config/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": "^7.0" + } +} diff --git a/e2e/composer-and-phpstan-version-config/phpstan.neon b/e2e/composer-and-phpstan-version-config/phpstan.neon new file mode 100644 index 0000000000..003e5e1484 --- /dev/null +++ b/e2e/composer-and-phpstan-version-config/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + phpVersion: + min: 80103 + max: 80304 diff --git a/e2e/composer-and-phpstan-version-config/test.php b/e2e/composer-and-phpstan-version-config/test.php new file mode 100644 index 0000000000..a9afaa4b65 --- /dev/null +++ b/e2e/composer-and-phpstan-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/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/phpstan.neon b/e2e/discussion-11362/phpstan.neon index d9a4bd0ab3..2e6178c1a7 100644 --- a/e2e/discussion-11362/phpstan.neon +++ b/e2e/discussion-11362/phpstan.neon @@ -1,7 +1,5 @@ parameters: excludePaths: - analyseAndScan: - - .git analyse: - vendor 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/result-cache-5/phpstan.neon b/e2e/result-cache-5/phpstan.neon index ddbf4c2114..7c3f71ae98 100644 --- a/e2e/result-cache-5/phpstan.neon +++ b/e2e/result-cache-5/phpstan.neon @@ -5,3 +5,7 @@ parameters: level: 8 paths: - src + ignoreErrors: + - + identifier: instanceof.alwaysTrue + reportUnmatched: false diff --git a/e2e/result-cache-7/phpstan.neon b/e2e/result-cache-7/phpstan.neon index ddbf4c2114..66c19c7166 100644 --- a/e2e/result-cache-7/phpstan.neon +++ b/e2e/result-cache-7/phpstan.neon @@ -5,3 +5,6 @@ parameters: level: 8 paths: - src + ignoreErrors: + - + identifier: trait.unused diff --git a/e2e/result-cache-meta-extension/.gitignore b/e2e/result-cache-meta-extension/.gitignore new file mode 100644 index 0000000000..61ead86667 --- /dev/null +++ b/e2e/result-cache-meta-extension/.gitignore @@ -0,0 +1 @@ +/vendor diff --git a/e2e/result-cache-meta-extension/composer.json b/e2e/result-cache-meta-extension/composer.json new file mode 100644 index 0000000000..a072011fe8 --- /dev/null +++ b/e2e/result-cache-meta-extension/composer.json @@ -0,0 +1,5 @@ +{ + "autoload-dev": { + "classmap": ["src/"] + } +} diff --git a/e2e/result-cache-meta-extension/composer.lock b/e2e/result-cache-meta-extension/composer.lock new file mode 100644 index 0000000000..b383d88ac5 --- /dev/null +++ b/e2e/result-cache-meta-extension/composer.lock @@ -0,0 +1,18 @@ +{ + "_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": "d751713988987e9331980363e24189ce", + "packages": [], + "packages-dev": [], + "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-meta-extension/hash.txt b/e2e/result-cache-meta-extension/hash.txt new file mode 100644 index 0000000000..1f34c8dfd2 --- /dev/null +++ b/e2e/result-cache-meta-extension/hash.txt @@ -0,0 +1 @@ +initial-hash diff --git a/e2e/result-cache-meta-extension/phpstan.neon b/e2e/result-cache-meta-extension/phpstan.neon new file mode 100644 index 0000000000..f2f9c41148 --- /dev/null +++ b/e2e/result-cache-meta-extension/phpstan.neon @@ -0,0 +1,10 @@ +parameters: + level: 8 + paths: + - src + +services: + - + class: ResultCacheE2E\MetaExtension\DummyResultCacheMetaExtension + tags: + - phpstan.resultCacheMetaExtension diff --git a/e2e/result-cache-meta-extension/src/DummyResultCacheMetaExtension.php b/e2e/result-cache-meta-extension/src/DummyResultCacheMetaExtension.php new file mode 100644 index 0000000000..81b6332f96 --- /dev/null +++ b/e2e/result-cache-meta-extension/src/DummyResultCacheMetaExtension.php @@ -0,0 +1,21 @@ +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 @@ +=7.0.0", + "php": ">=7.1", "psr/http-message": "^1.0 || ^2.0" }, "type": "library", @@ -1753,7 +1750,7 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -1765,9 +1762,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + "source": "https://github.com/php-fig/http-factory" }, - "time": "2023-04-10T20:10:41+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { "name": "psr/http-message", @@ -1868,16 +1865,16 @@ }, { "name": "symfony/console", - "version": "v6.4.9", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9" + "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9", - "reference": "6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9", + "url": "https://api.github.com/repos/symfony/console/zipball/72d080eb9edf80e36c19be61f72c98ed8273b765", + "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765", "shasum": "" }, "require": { @@ -1942,7 +1939,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.9" + "source": "https://github.com/symfony/console/tree/v6.4.12" }, "funding": [ { @@ -1958,7 +1955,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T09:49:33+00:00" + "time": "2024-09-20T08:15:52+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2029,16 +2026,16 @@ }, { "name": "symfony/finder", - "version": "v6.4.8", + "version": "v6.4.11", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "3ef977a43883215d560a2cecb82ec8e62131471c" + "reference": "d7eb6daf8cd7e9ac4976e9576b32042ef7253453" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/3ef977a43883215d560a2cecb82ec8e62131471c", - "reference": "3ef977a43883215d560a2cecb82ec8e62131471c", + "url": "https://api.github.com/repos/symfony/finder/zipball/d7eb6daf8cd7e9ac4976e9576b32042ef7253453", + "reference": "d7eb6daf8cd7e9ac4976e9576b32042ef7253453", "shasum": "" }, "require": { @@ -2073,7 +2070,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.8" + "source": "https://github.com/symfony/finder/tree/v6.4.11" }, "funding": [ { @@ -2089,20 +2086,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-08-13T14:27:37+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.0.0", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "700ff4096e346f54cb628ea650767c8130f1001f" + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/700ff4096e346f54cb628ea650767c8130f1001f", - "reference": "700ff4096e346f54cb628ea650767c8130f1001f", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/47aa818121ed3950acd2b58d1d37d08a94f9bf55", + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55", "shasum": "" }, "require": { @@ -2140,7 +2137,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.0.0" + "source": "https://github.com/symfony/options-resolver/tree/v7.1.1" }, "funding": [ { @@ -2156,24 +2153,24 @@ "type": "tidelift" } ], - "time": "2023-08-08T10:20:21+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -2219,7 +2216,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" }, "funding": [ { @@ -2235,24 +2232,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", + "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" @@ -2297,7 +2294,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" }, "funding": [ { @@ -2313,24 +2310,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "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" @@ -2378,7 +2375,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" }, "funding": [ { @@ -2394,24 +2391,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -2458,7 +2455,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -2474,24 +2471,24 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:30:46+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -2538,7 +2535,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -2554,7 +2551,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/service-contracts", @@ -2641,16 +2638,16 @@ }, { "name": "symfony/string", - "version": "v7.1.2", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8" + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/14221089ac66cf82e3cf3d1c1da65de305587ff8", - "reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8", + "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306", + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306", "shasum": "" }, "require": { @@ -2708,7 +2705,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.2" + "source": "https://github.com/symfony/string/tree/v7.1.5" }, "funding": [ { @@ -2724,7 +2721,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T09:27:18+00:00" + "time": "2024-09-20T08:28:38+00:00" } ], "packages-dev": [ @@ -2800,16 +2797,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", "shasum": "" }, "require": { @@ -2817,11 +2814,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "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", @@ -2847,7 +2845,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" }, "funding": [ { @@ -2855,20 +2853,20 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-06-12T14:39:25+00:00" }, { "name": "nikic/php-parser", - "version": "v5.0.2", + "version": "v5.2.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" + "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", + "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", "shasum": "" }, "require": { @@ -2879,7 +2877,7 @@ }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -2911,9 +2909,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.2.0" }, - "time": "2024-03-05T20:51:40+00:00" + "time": "2024-09-15T16:40:33+00:00" }, { "name": "phar-io/manifest", @@ -3035,35 +3033,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.31", + "version": "9.2.32", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", + "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.18 || ^5.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -3072,7 +3070,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -3101,7 +3099,7 @@ "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.31" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { @@ -3109,7 +3107,7 @@ "type": "github" } ], - "time": "2024-03-02T06:37:42+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -3354,45 +3352,45 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.19", + "version": "9.6.21", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8" + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8", - "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1 || ^2", + "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.3", - "phar-io/version": "^3.0.2", + "myclabs/deep-copy": "^1.12.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.28", - "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", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.5", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.2", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.6", + "sebastian/global-state": "^5.0.7", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", "sebastian/version": "^3.0.2" }, "suggest": { @@ -3437,7 +3435,7 @@ "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.19" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" }, "funding": [ { @@ -3453,7 +3451,7 @@ "type": "tidelift" } ], - "time": "2024-04-05T04:35:58+00:00" + "time": "2024-09-19T10:50:18+00:00" }, { "name": "sebastian/cli-parser", @@ -4471,14 +4469,12 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "phpstan/phpstan-strict-rules": 20 - }, + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.3" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/issue-bot/playground.neon b/issue-bot/playground.neon index 2f9743d575..a252e3bac8 100644 --- a/issue-bot/playground.neon +++ b/issue-bot/playground.neon @@ -3,3 +3,12 @@ rules: - 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/Comment/BotCommentParser.php b/issue-bot/src/Comment/BotCommentParser.php index c09d09263d..f0985151df 100644 --- a/issue-bot/src/Comment/BotCommentParser.php +++ b/issue-bot/src/Comment/BotCommentParser.php @@ -21,7 +21,7 @@ public function parse(string $text): BotCommentParserResult $walker = $document->walker(); $hashes = []; $diffs = []; - while ($event = $walker->next()) { + while ($event = $walker->next()) { // @phpstan-ignore while.condNotBoolean if (!$event->isEntering()) { continue; } diff --git a/issue-bot/src/Console/DownloadCommand.php b/issue-bot/src/Console/DownloadCommand.php index 2b702bd9a2..3edaa7c1e3 100644 --- a/issue-bot/src/Console/DownloadCommand.php +++ b/issue-bot/src/Console/DownloadCommand.php @@ -96,12 +96,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $matrix = []; - foreach ([70200, 70300, 70400, 80000, 80100, 80200, 80300] as $phpVersion) { + 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 = [70200, 70300, 70400, 80000]; + $resultPhpVersions = [70300, 70400, 80000]; } if (!in_array(80100, $resultPhpVersions, true)) { @@ -113,12 +113,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int 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'); diff --git a/issue-bot/src/Console/EvaluateCommand.php b/issue-bot/src/Console/EvaluateCommand.php index 0f8d05a8d8..9836d85bb0 100644 --- a/issue-bot/src/Console/EvaluateCommand.php +++ b/issue-bot/src/Console/EvaluateCommand.php @@ -101,7 +101,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $originalPhpVersions = array_keys($originalErrors); $newResult = $newResults[$hash]; if (array_key_exists(70100, $originalErrors) || $originalPhpVersions === [70400]) { - $newResult[70100] = $newResult[70200]; + $newResult[70100] = $newResult[70300]; + } + if (array_key_exists(70200, $originalErrors)) { + $newResult[70200] = $newResult[70300]; } $newTabs = $this->tabCreator->create($this->filterErrors($originalErrors, $newResult)); diff --git a/issue-bot/src/Playground/TabCreator.php b/issue-bot/src/Playground/TabCreator.php index 544fcb1461..a6254957b2 100644 --- a/issue-bot/src/Playground/TabCreator.php +++ b/issue-bot/src/Playground/TabCreator.php @@ -2,7 +2,9 @@ namespace PHPStan\IssueBot\Playground; +use function array_filter; use function array_map; +use function array_values; use function count; use function floor; use function ksort; @@ -26,6 +28,7 @@ public function create(array $versionedErrors): array $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; 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/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/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 index 17aff2c148..607a23fda2 100644 --- a/patches/PDO.patch +++ b/patches/PDO.patch @@ -1,11 +1,11 @@ --- PDO/PDO.php 2021-12-26 15:44:39.000000000 +0100 +++ PDO/PDO.php 2022-01-03 22:54:21.000000000 +0100 -@@ -1415,7 +1415,7 @@ - * @return array|false if one or more notifications is pending, returns a single row, - * with fields message and pid, otherwise FALSE. - */ -- public function pgsqlGetNotify(int $fetchMode = PDO::FETCH_DEFAULT, int $timeoutMilliseconds = 0): array|false {} -+ public function pgsqlGetNotify(int $fetchMode = 1, int $timeoutMilliseconds = 0): array|false {} +@@ -1476,7 +1476,7 @@ namespace { + * @return array|false if one or more notifications is pending, returns a single row, + * with fields message and pid, otherwise FALSE. + */ +- public function pgsqlGetNotify($fetchMode = PDO::FETCH_DEFAULT, $timeoutMilliseconds = 0) {} ++ public function pgsqlGetNotify($fetchMode = 1, $timeoutMilliseconds = 0) {} - /** - * (PHP 5 >= 5.6.0, PHP 7, PHP 8)
+ /** + * (PHP 5 >= 5.6.0, PHP 7, PHP 8)
diff --git a/patches/Read.patch b/patches/Read.patch new file mode 100644 index 0000000000..ad9b64c445 --- /dev/null +++ b/patches/Read.patch @@ -0,0 +1,11 @@ +--- Read.php 2017-07-11 09:42:15 ++++ Read.php 2024-08-26 23:09:54 +@@ -77,7 +77,7 @@ + * @throws \Hoa\File\Exception\FileDoesNotExist + * @throws \Hoa\File\Exception + */ +- protected function &_open($streamName, Stream\Context $context = null) ++ protected function &_open($streamName, ?Stream\Context $context = null) + { + static $createModes = [ + parent::MODE_READ diff --git a/patches/Sender.patch b/patches/Sender.patch new file mode 100644 index 0000000000..e01d1ff81c --- /dev/null +++ b/patches/Sender.patch @@ -0,0 +1,11 @@ +--- src/Io/Sender.php 2024-03-27 18:20:46 ++++ src/Io/Sender.php 2024-10-14 10:19:28 +@@ -48,7 +48,7 @@ + * @param ConnectorInterface|null $connector + * @return self + */ +- public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null) ++ public static function createFromLoop(LoopInterface $loop, ?ConnectorInterface $connector = null) + { + if ($connector === null) { + $connector = new Connector(array(), $loop); diff --git a/patches/Stream.patch b/patches/Stream.patch index daf6990e1b..8dbb2e108e 100644 --- a/patches/Stream.patch +++ b/patches/Stream.patch @@ -1,5 +1,5 @@ ---- 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 @@ -9,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/phpcs.xml b/phpcs.xml index 8a8a89df38..96cbd5e6d6 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,6 +1,7 @@ + @@ -202,10 +203,12 @@ + src/Type/TypeResult.php compiler/tests/*/data/ tests/*/Fixture/ tests/*/cache/ + tests/vendor/* tests/*/data/ tests/*/traits/ tests/PHPStan/Analyser/nsrt/ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index cdf3372afc..47101ea9fe 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,1862 +1,2020 @@ parameters: ignoreErrors: - - message: "#^Method PHPStan\\\\Analyser\\\\AnalyserResultFinalizer\\:\\:finalize\\(\\) throws checked exception Throwable but it's missing from the PHPDoc @throws tag\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Analyser/AnalyserResultFinalizer.php + path: build/PHPStan/Build/ContainerDynamicReturnTypeExtension.php - - message: "#^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\\.$#" + message: '#^Method PHPStan\\Analyser\\AnalyserResultFinalizer\:\:finalize\(\) throws checked exception Throwable but it''s missing from the PHPDoc @throws tag\.$#' + identifier: missingType.checkedException count: 1 - path: src/Analyser/DirectInternalScopeFactory.php + path: src/Analyser/AnalyserResultFinalizer.php - - message: "#^Cannot assign offset 'realCount' to array\\|string\\.$#" + message: '#^Cannot assign offset ''realCount'' to array\\|string\.$#' + identifier: offsetAssign.dimType count: 1 path: src/Analyser/Ignore/IgnoredErrorHelperResult.php - - message: "#^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\\.$#" - count: 1 - path: src/Analyser/LazyInternalScopeFactory.php - - - - message: """ - #^Call to deprecated method getAnyArrays\\(\\) of class PHPStan\\\\Type\\\\TypeUtils\\: - Use PHPStan\\\\Type\\\\Type\\:\\:getArrays\\(\\) instead\\.$# - """ - count: 2 + message: '#^Casting to string something that''s already string\.$#' + identifier: cast.useless + count: 3 path: src/Analyser/MutatingScope.php - - message: """ - #^Call to deprecated method getTypeFromValue\\(\\) of class PHPStan\\\\Type\\\\ConstantTypeHelper\\: - Use PHPStan\\\\Reflection\\\\InitializerExprTypeResolver$# - """ - count: 1 + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 path: src/Analyser/MutatingScope.php - - message: "#^Casting to string something that's already string\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Analyser/MutatingScope.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" - count: 4 + message: '#^Only numeric types are allowed in pre\-increment, float\|int\|string\|null given\.$#' + identifier: preInc.nonNumeric + count: 1 path: src/Analyser/MutatingScope.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" - count: 3 - path: src/Analyser/MutatingScope.php + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Analyser/NodeScopeResolver.php - - message: "#^Only numeric types are allowed in pre\\-increment, float\\|int\\|string\\|null given\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Analyser/MutatingScope.php + path: src/Analyser/NodeScopeResolver.php - - message: """ - #^Call to deprecated method doNotTreatPhpDocTypesAsCertain\\(\\) of class PHPStan\\\\Analyser\\\\MutatingScope\\: - Use getNativeType\\(\\)$# - """ + message: '#^Parameter \#2 \$node of method PHPStan\\BetterReflection\\SourceLocator\\Ast\\Strategy\\NodeToReflection\:\:__invoke\(\) expects PhpParser\\Node\\Expr\\ArrowFunction\|PhpParser\\Node\\Expr\\Closure\|PhpParser\\Node\\Expr\\FuncCall\|PhpParser\\Node\\Stmt\\Class_\|PhpParser\\Node\\Stmt\\Const_\|PhpParser\\Node\\Stmt\\Enum_\|PhpParser\\Node\\Stmt\\Function_\|PhpParser\\Node\\Stmt\\Interface_\|PhpParser\\Node\\Stmt\\Trait_, PhpParser\\Node\\Stmt\\ClassLike given\.$#' + identifier: argument.type count: 1 path: src/Analyser/NodeScopeResolver.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" - count: 3 - path: src/Analyser/NodeScopeResolver.php + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Analyser/RicherScopeGetTypeHelper.php - - message: "#^Parameter \\#2 \\$node of method PHPStan\\\\BetterReflection\\\\SourceLocator\\\\Ast\\\\Strategy\\\\NodeToReflection\\:\\:__invoke\\(\\) expects PhpParser\\\\Node\\\\Expr\\\\ArrowFunction\\|PhpParser\\\\Node\\\\Expr\\\\Closure\\|PhpParser\\\\Node\\\\Expr\\\\FuncCall\\|PhpParser\\\\Node\\\\Stmt\\\\Class_\\|PhpParser\\\\Node\\\\Stmt\\\\Const_\\|PhpParser\\\\Node\\\\Stmt\\\\Enum_\\|PhpParser\\\\Node\\\\Stmt\\\\Function_\\|PhpParser\\\\Node\\\\Stmt\\\\Interface_\\|PhpParser\\\\Node\\\\Stmt\\\\Trait_, PhpParser\\\\Node\\\\Stmt\\\\ClassLike given\\.$#" + message: '#^Call to method __construct\(\) of internal class PhpParser\\Internal\\TokenStream from outside its root namespace PhpParser\.$#' + identifier: method.internalClass count: 1 - path: src/Analyser/NodeScopeResolver.php + path: src/Analyser/RuleErrorTransformer.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Instantiation of internal class PhpParser\\Internal\\TokenStream\.$#' + identifier: new.internalClass + count: 1 + path: src/Analyser/RuleErrorTransformer.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Analyser/TypeSpecifier.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 5 path: src/Analyser/TypeSpecifier.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" - count: 3 + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 path: src/Analyser/TypeSpecifier.php - - message: "#^Template type TNodeType is declared as covariant, but occurs in contravariant position in parameter node of method PHPStan\\\\Collectors\\\\Collector\\:\\:processNode\\(\\)\\.$#" + message: '#^Template type TNodeType is declared as covariant, but occurs in contravariant position in parameter node of method PHPStan\\Collectors\\Collector\:\:processNode\(\)\.$#' + identifier: generics.variance count: 1 path: src/Collectors/Collector.php - - message: "#^Method PHPStan\\\\Collectors\\\\Registry\\:\\:__construct\\(\\) has parameter \\$collectors with generic interface PHPStan\\\\Collectors\\\\Collector but does not specify its types\\: TNodeType, TValue$#" + message: '#^Method PHPStan\\Collectors\\Registry\:\:__construct\(\) has parameter \$collectors with generic interface PHPStan\\Collectors\\Collector but does not specify its types\: TNodeType, TValue$#' + identifier: missingType.generics count: 1 path: src/Collectors/Registry.php - - message: "#^Property PHPStan\\\\Collectors\\\\Registry\\:\\:\\$cache with generic interface PHPStan\\\\Collectors\\\\Collector does not specify its types\\: TNodeType, TValue$#" + message: '#^Property PHPStan\\Collectors\\Registry\:\:\$cache with generic interface PHPStan\\Collectors\\Collector does not specify its types\: TNodeType, TValue$#' + identifier: missingType.generics count: 1 path: src/Collectors/Registry.php - - message: "#^Property PHPStan\\\\Collectors\\\\Registry\\:\\:\\$collectors with generic interface PHPStan\\\\Collectors\\\\Collector does not specify its types\\: TNodeType, TValue$#" + message: '#^Property PHPStan\\Collectors\\Registry\:\:\$collectors with generic interface PHPStan\\Collectors\\Collector does not specify its types\: TNodeType, TValue$#' + identifier: missingType.generics count: 1 path: src/Collectors/Registry.php - - message: "#^Anonymous function has an unused use \\$container\\.$#" + message: '#^Anonymous function has an unused use \$container\.$#' + identifier: closure.unusedUse count: 1 path: src/Command/CommandHelper.php - - message: "#^Parameter \\#1 \\$path of function dirname expects string, string\\|false given\\.$#" + message: '#^Call to static method expand\(\) of internal class Nette\\DI\\Helpers from outside its root namespace Nette\.$#' + identifier: staticMethod.internalClass + count: 2 + path: src/Command/CommandHelper.php + + - + message: '#^Parameter \#1 \$path of function dirname expects string, string\|false given\.$#' + identifier: argument.type count: 1 path: src/Command/CommandHelper.php - - message: "#^Static property PHPStan\\\\Command\\\\CommandHelper\\:\\:\\$reservedMemory is never read, only written\\.$#" + message: '#^Static property PHPStan\\Command\\CommandHelper\:\:\$reservedMemory is never read, only written\.$#' + identifier: property.onlyWritten count: 1 path: src/Command/CommandHelper.php - - message: "#^Parameter \\#1 \\$headers \\(array\\\\) of method PHPStan\\\\Command\\\\ErrorsConsoleStyle\\:\\:table\\(\\) should be contravariant with parameter \\$headers \\(array\\) of method Symfony\\\\Component\\\\Console\\\\Style\\\\StyleInterface\\:\\:table\\(\\)$#" + message: '#^Call to static method escape\(\) of internal class Nette\\DI\\Helpers from outside its root namespace Nette\.$#' + identifier: staticMethod.internalClass + count: 4 + path: src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php + + - + message: '#^Call to static method escape\(\) of internal class Nette\\DI\\Helpers from outside its root namespace Nette\.$#' + identifier: staticMethod.internalClass + count: 5 + path: src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php + + - + message: '#^Parameter \#1 \$headers \(array\\) of method PHPStan\\Command\\ErrorsConsoleStyle\:\:table\(\) should be contravariant with parameter \$headers \(array\) of method Symfony\\Component\\Console\\Style\\StyleInterface\:\:table\(\)$#' + identifier: method.childParameterType count: 1 path: src/Command/ErrorsConsoleStyle.php - - message: "#^Parameter \\#1 \\$headers \\(array\\\\) of method PHPStan\\\\Command\\\\ErrorsConsoleStyle\\:\\:table\\(\\) should be contravariant with parameter \\$headers \\(array\\) of method Symfony\\\\Component\\\\Console\\\\Style\\\\SymfonyStyle\\:\\:table\\(\\)$#" + message: '#^Parameter \#1 \$headers \(array\\) of method PHPStan\\Command\\ErrorsConsoleStyle\:\:table\(\) should be contravariant with parameter \$headers \(array\) of method Symfony\\Component\\Console\\Style\\SymfonyStyle\:\:table\(\)$#' + identifier: method.childParameterType count: 1 path: src/Command/ErrorsConsoleStyle.php - - message: "#^Parameter \\#2 \\$rows \\(array\\\\>\\) of method PHPStan\\\\Command\\\\ErrorsConsoleStyle\\:\\:table\\(\\) should be contravariant with parameter \\$rows \\(array\\) of method Symfony\\\\Component\\\\Console\\\\Style\\\\StyleInterface\\:\\:table\\(\\)$#" + message: '#^Parameter \#2 \$rows \(array\\>\) of method PHPStan\\Command\\ErrorsConsoleStyle\:\:table\(\) should be contravariant with parameter \$rows \(array\) of method Symfony\\Component\\Console\\Style\\StyleInterface\:\:table\(\)$#' + identifier: method.childParameterType count: 1 path: src/Command/ErrorsConsoleStyle.php - - message: "#^Parameter \\#2 \\$rows \\(array\\\\>\\) of method PHPStan\\\\Command\\\\ErrorsConsoleStyle\\:\\:table\\(\\) should be contravariant with parameter \\$rows \\(array\\) of method Symfony\\\\Component\\\\Console\\\\Style\\\\SymfonyStyle\\:\\:table\\(\\)$#" + message: '#^Parameter \#2 \$rows \(array\\>\) of method PHPStan\\Command\\ErrorsConsoleStyle\:\:table\(\) should be contravariant with parameter \$rows \(array\) of method Symfony\\Component\\Console\\Style\\SymfonyStyle\:\:table\(\)$#' + identifier: method.childParameterType count: 1 path: src/Command/ErrorsConsoleStyle.php - - message: "#^Variable method call on Nette\\\\Schema\\\\Elements\\\\AnyOf\\|Nette\\\\Schema\\\\Elements\\\\Structure\\|Nette\\\\Schema\\\\Elements\\\\Type\\.$#" + message: '#^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 + + - + message: '#^Call to static method expand\(\) of internal class Nette\\DI\\Helpers from outside its root namespace Nette\.$#' + identifier: staticMethod.internalClass + count: 2 + path: src/DependencyInjection/AutowiredAttributeServicesExtension.php + + - + message: '#^Call to static method expand\(\) of internal class Nette\\DI\\Helpers from outside its root namespace Nette\.$#' + identifier: staticMethod.internalClass count: 1 path: src/DependencyInjection/ContainerFactory.php - - message: "#^Variable static method call on Nette\\\\Schema\\\\Expect\\.$#" + message: '#^Call to static method merge\(\) of internal class Nette\\Schema\\Helpers from outside its root namespace Nette\.$#' + identifier: staticMethod.internalClass + count: 2 + path: src/DependencyInjection/ContainerFactory.php + + - + message: '#^Variable method call on Nette\\Schema\\Elements\\AnyOf\|Nette\\Schema\\Elements\\Structure\|Nette\\Schema\\Elements\\Type\.$#' + identifier: method.dynamicName count: 1 path: src/DependencyInjection/ContainerFactory.php - - message: "#^Fetching class constant PREVENT_MERGING of deprecated class Nette\\\\DI\\\\Config\\\\Helpers\\.$#" + message: '#^Variable static method call on Nette\\Schema\\Expect\.$#' + identifier: staticMethod.dynamicName count: 1 - path: src/DependencyInjection/NeonAdapter.php + path: src/DependencyInjection/ContainerFactory.php - - message: "#^Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\\. Use objects retrieved from ReflectionProvider instead\\.$#" + message: '#^Fetching class constant PREVENT_MERGING of deprecated class Nette\\DI\\Config\\Helpers\.$#' + identifier: classConstant.deprecatedClass count: 1 - path: src/Diagnose/PHPStanDiagnoseExtension.php + path: src/DependencyInjection/NeonAdapter.php - - message: "#^Parameter \\#1 \\$path of function dirname expects string, string\\|false given\\.$#" + message: '#^Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 path: src/Diagnose/PHPStanDiagnoseExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Parameter \#1 \$path of function dirname expects string, string\|false given\.$#' + identifier: argument.type count: 1 - path: src/Internal/ContainerDynamicReturnTypeExtension.php + path: src/Diagnose/PHPStanDiagnoseExtension.php - - message: "#^Variable method call on PHPStan\\\\Reflection\\\\ClassReflection\\.$#" + message: '#^Call to method getContent\(\) of internal class PhpMerge\\internal\\Line from outside its root namespace PhpMerge\.$#' + identifier: method.internalClass count: 2 - path: src/PhpDoc/PhpDocBlock.php + path: src/Fixable/Patcher.php - - message: "#^Variable static method call on PHPStan\\\\PhpDoc\\\\PhpDocBlock\\.$#" - count: 1 - path: src/PhpDoc/PhpDocBlock.php + message: '#^Call to static method createArray\(\) of internal class PhpMerge\\internal\\Hunk from outside its root namespace PhpMerge\.$#' + identifier: staticMethod.internalClass + count: 2 + path: src/Fixable/Patcher.php - - message: "#^Call to function method_exists\\(\\) with PHPStan\\\\PhpDocParser\\\\Ast\\\\PhpDoc\\\\PhpDocNode and 'getParamOutTypeTagV…' will always evaluate to true\\.$#" - count: 1 - path: src/PhpDoc/PhpDocNodeResolver.php + message: '#^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 - - message: "#^Call to function method_exists\\(\\) with PHPStan\\\\PhpDocParser\\\\Ast\\\\PhpDoc\\\\PhpDocNode and 'getSelfOutTypeTagVa…' will always evaluate to true\\.$#" + message: '#^Call to method getTokenCode\(\) of internal class PhpParser\\Internal\\TokenStream from outside its root namespace PhpParser\.$#' + identifier: method.internalClass count: 1 - path: src/PhpDoc/PhpDocNodeResolver.php + path: src/Fixable/PhpPrinterIndentationDetectorVisitor.php - - message: "#^Property PHPStan\\\\PhpDocParser\\\\Ast\\\\PhpDoc\\\\AssertTagMethodValueNode\\:\\:\\$isEquality \\(bool\\) on left side of \\?\\? is not nullable\\.$#" + message: '#^Parameter \$origTokens of method PHPStan\\Fixable\\PhpPrinterIndentationDetectorVisitor\:\:__construct\(\) has typehint with internal class PhpParser\\Internal\\TokenStream\.$#' + identifier: parameter.internalClass count: 1 - path: src/PhpDoc/PhpDocNodeResolver.php + path: src/Fixable/PhpPrinterIndentationDetectorVisitor.php - - message: "#^Property PHPStan\\\\PhpDocParser\\\\Ast\\\\PhpDoc\\\\AssertTagPropertyValueNode\\:\\:\\$isEquality \\(bool\\) on left side of \\?\\? is not nullable\\.$#" + message: '#^Property \$origTokens references internal class PhpParser\\Internal\\TokenStream in its type\.$#' + identifier: property.internalClass count: 1 - path: src/PhpDoc/PhpDocNodeResolver.php + path: src/Fixable/PhpPrinterIndentationDetectorVisitor.php - - message: "#^Property PHPStan\\\\PhpDocParser\\\\Ast\\\\PhpDoc\\\\AssertTagValueNode\\:\\:\\$isEquality \\(bool\\) on left side of \\?\\? is not nullable\\.$#" + message: '#^Access to property \$id of internal class Symfony\\Polyfill\\Php80\\PhpToken from outside its root namespace Symfony\.$#' + identifier: property.internalClass count: 1 - path: src/PhpDoc/PhpDocNodeResolver.php + path: src/Parser/RichParser.php - - message: "#^Method PHPStan\\\\PhpDoc\\\\ResolvedPhpDocBlock\\:\\:getNameScope\\(\\) should return PHPStan\\\\Analyser\\\\NameScope but returns PHPStan\\\\Analyser\\\\NameScope\\|null\\.$#" - count: 1 - path: src/PhpDoc/ResolvedPhpDocBlock.php + message: '#^Access to property \$line of internal class Symfony\\Polyfill\\Php80\\PhpToken from outside its root namespace Symfony\.$#' + identifier: property.internalClass + count: 4 + path: src/Parser/RichParser.php - - message: """ - #^Call to deprecated method getInstance\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Use PHPStan\\\\Reflection\\\\ReflectionProviderStaticAccessor instead$# - """ - count: 1 - path: src/PhpDoc/StubValidator.php + message: '#^Access to property \$text of internal class Symfony\\Polyfill\\Php80\\PhpToken from outside its root namespace Symfony\.$#' + identifier: property.internalClass + count: 3 + path: src/Parser/RichParser.php - - message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\ParamOutTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\ParamOutTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" + message: '#^Call to function method_exists\(\) with PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocNode and ''getParamOutTypeTagV…'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: src/PhpDoc/Tag/ParamOutTag.php + path: src/PhpDoc/PhpDocNodeResolver.php - - message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\ParamTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\ParamTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" + message: '#^Call to function method_exists\(\) with PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocNode and ''getSelfOutTypeTagVa…'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: src/PhpDoc/Tag/ParamTag.php + path: src/PhpDoc/PhpDocNodeResolver.php - - message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\ReturnTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\ReturnTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" + message: '#^Method PHPStan\\PhpDoc\\ResolvedPhpDocBlock\:\:getNameScope\(\) should return PHPStan\\Analyser\\NameScope but returns PHPStan\\Analyser\\NameScope\|null\.$#' + identifier: return.type count: 1 - path: src/PhpDoc/Tag/ReturnTag.php + path: src/PhpDoc/ResolvedPhpDocBlock.php - - message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\SelfOutTypeTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\SelfOutTypeTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" + message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/PhpDoc/Tag/SelfOutTypeTag.php + path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\VarTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\VarTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" + message: '#^Doing instanceof PHPStan\\Type\\CallableType is error\-prone and deprecated\. Use Type\:\:isCallable\(\) and Type\:\:getCallableParametersAcceptors\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/PhpDoc/Tag/VarTag.php + path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\CallableType is error\\-prone and deprecated\\. Use Type\\:\\:isCallable\\(\\) and Type\\:\\:getCallableParametersAcceptors\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IterableType is error\\-prone and deprecated\\. Use Type\\:\\:isIterable\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" - count: 2 + message: '#^Doing instanceof PHPStan\\Type\\IterableType is error\-prone and deprecated\. Use Type\:\:isIterable\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Property PHPStan\\\\PhpDocParser\\\\Ast\\\\Type\\\\CallableTypeNode\\:\\:\\$templateTypes \\(array\\\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Dead catch \\- PHPStan\\\\BetterReflection\\\\Identifier\\\\Exception\\\\InvalidIdentifierName is never thrown in the try block\\.$#" - count: 3 + message: '#^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 is never thrown in the try block\\.$#" + message: '#^Dead catch \- PHPStan\\BetterReflection\\NodeCompiler\\Exception\\UnableToCompileNode is never thrown in the try block\.$#' + identifier: catch.neverThrown count: 1 path: src/Reflection/BetterReflection/BetterReflectionProvider.php - - message: """ - #^Call to deprecated method getTypeFromValue\\(\\) of class PHPStan\\\\Type\\\\ConstantTypeHelper\\: - Use PHPStan\\\\Reflection\\\\InitializerExprTypeResolver$# - """ + message: '#^Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 path: src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php - - message: "#^Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\\. Use objects retrieved from ReflectionProvider instead\\.$#" + message: '#^Creating new ReflectionFunction is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 path: src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php - - message: "#^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\\.$#" + message: '#^Parameter \#2 \$node of method PHPStan\\BetterReflection\\SourceLocator\\Ast\\Strategy\\NodeToReflection\:\:__invoke\(\) expects PhpParser\\Node\\Expr\\ArrowFunction\|PhpParser\\Node\\Expr\\Closure\|PhpParser\\Node\\Expr\\FuncCall\|PhpParser\\Node\\Stmt\\Class_\|PhpParser\\Node\\Stmt\\Const_\|PhpParser\\Node\\Stmt\\Enum_\|PhpParser\\Node\\Stmt\\Function_\|PhpParser\\Node\\Stmt\\Interface_\|PhpParser\\Node\\Stmt\\Trait_, PhpParser\\Node\\Stmt\\ClassLike given\.$#' + identifier: argument.type count: 1 path: src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php - - message: "#^Parameter \\#2 \\$node of method PHPStan\\\\BetterReflection\\\\SourceLocator\\\\Ast\\\\Strategy\\\\NodeToReflection\\:\\:__invoke\\(\\) expects PhpParser\\\\Node\\\\Expr\\\\ArrowFunction\\|PhpParser\\\\Node\\\\Expr\\\\Closure\\|PhpParser\\\\Node\\\\Expr\\\\FuncCall\\|PhpParser\\\\Node\\\\Stmt\\\\Class_\\|PhpParser\\\\Node\\\\Stmt\\\\Const_\\|PhpParser\\\\Node\\\\Stmt\\\\Enum_\\|PhpParser\\\\Node\\\\Stmt\\\\Function_\\|PhpParser\\\\Node\\\\Stmt\\\\Interface_\\|PhpParser\\\\Node\\\\Stmt\\\\Trait_, PhpParser\\\\Node\\\\Stmt\\\\ClassLike given\\.$#" - count: 1 - path: src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php - - - - message: "#^Method PHPStan\\\\Reflection\\\\BetterReflection\\\\SourceLocator\\\\FileReadTrapStreamWrapper\\:\\:invokeWithRealFileStreamWrapper\\(\\) has parameter \\$cb with no signature specified for callable\\.$#" + message: '#^Method PHPStan\\Reflection\\BetterReflection\\SourceLocator\\FileReadTrapStreamWrapper\:\:invokeWithRealFileStreamWrapper\(\) has parameter \$cb with no signature specified for callable\.$#' + identifier: missingType.callable count: 1 path: src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php - - message: "#^Parameter \\#2 \\$node of method PHPStan\\\\BetterReflection\\\\SourceLocator\\\\Ast\\\\Strategy\\\\NodeToReflection\\:\\:__invoke\\(\\) expects PhpParser\\\\Node\\\\Expr\\\\ArrowFunction\\|PhpParser\\\\Node\\\\Expr\\\\Closure\\|PhpParser\\\\Node\\\\Expr\\\\FuncCall\\|PhpParser\\\\Node\\\\Stmt\\\\Class_\\|PhpParser\\\\Node\\\\Stmt\\\\Const_\\|PhpParser\\\\Node\\\\Stmt\\\\Enum_\\|PhpParser\\\\Node\\\\Stmt\\\\Function_\\|PhpParser\\\\Node\\\\Stmt\\\\Interface_\\|PhpParser\\\\Node\\\\Stmt\\\\Trait_, PhpParser\\\\Node\\\\Expr\\\\FuncCall\\|PhpParser\\\\Node\\\\Stmt\\\\ClassLike\\|PhpParser\\\\Node\\\\Stmt\\\\Const_\\|PhpParser\\\\Node\\\\Stmt\\\\Function_ given\\.$#" - count: 1 - path: src/Reflection/BetterReflection/SourceLocator/NewOptimizedDirectorySourceLocator.php - - - - message: "#^Parameter \\#2 \\$node of method PHPStan\\\\BetterReflection\\\\SourceLocator\\\\Ast\\\\Strategy\\\\NodeToReflection\\:\\:__invoke\\(\\) expects PhpParser\\\\Node\\\\Expr\\\\ArrowFunction\\|PhpParser\\\\Node\\\\Expr\\\\Closure\\|PhpParser\\\\Node\\\\Expr\\\\FuncCall\\|PhpParser\\\\Node\\\\Stmt\\\\Class_\\|PhpParser\\\\Node\\\\Stmt\\\\Const_\\|PhpParser\\\\Node\\\\Stmt\\\\Enum_\\|PhpParser\\\\Node\\\\Stmt\\\\Function_\\|PhpParser\\\\Node\\\\Stmt\\\\Interface_\\|PhpParser\\\\Node\\\\Stmt\\\\Trait_, PhpParser\\\\Node\\\\Expr\\\\FuncCall\\|PhpParser\\\\Node\\\\Stmt\\\\ClassLike\\|PhpParser\\\\Node\\\\Stmt\\\\Const_\\|PhpParser\\\\Node\\\\Stmt\\\\Function_ given\\.$#" + message: '#^Parameter \#2 \$node of method PHPStan\\BetterReflection\\SourceLocator\\Ast\\Strategy\\NodeToReflection\:\:__invoke\(\) expects PhpParser\\Node\\Expr\\ArrowFunction\|PhpParser\\Node\\Expr\\Closure\|PhpParser\\Node\\Expr\\FuncCall\|PhpParser\\Node\\Stmt\\Class_\|PhpParser\\Node\\Stmt\\Const_\|PhpParser\\Node\\Stmt\\Enum_\|PhpParser\\Node\\Stmt\\Function_\|PhpParser\\Node\\Stmt\\Interface_\|PhpParser\\Node\\Stmt\\Trait_, PhpParser\\Node\\Expr\\FuncCall\|PhpParser\\Node\\Stmt\\ClassLike\|PhpParser\\Node\\Stmt\\Const_\|PhpParser\\Node\\Stmt\\Function_ given\.$#' + identifier: argument.type count: 1 path: src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php - - message: "#^Parameter \\#2 \\$node of method PHPStan\\\\BetterReflection\\\\SourceLocator\\\\Ast\\\\Strategy\\\\NodeToReflection\\:\\:__invoke\\(\\) expects PhpParser\\\\Node\\\\Expr\\\\ArrowFunction\\|PhpParser\\\\Node\\\\Expr\\\\Closure\\|PhpParser\\\\Node\\\\Expr\\\\FuncCall\\|PhpParser\\\\Node\\\\Stmt\\\\Class_\\|PhpParser\\\\Node\\\\Stmt\\\\Const_\\|PhpParser\\\\Node\\\\Stmt\\\\Enum_\\|PhpParser\\\\Node\\\\Stmt\\\\Function_\\|PhpParser\\\\Node\\\\Stmt\\\\Interface_\\|PhpParser\\\\Node\\\\Stmt\\\\Trait_, PhpParser\\\\Node\\\\Stmt\\\\ClassLike given\\.$#" + message: '#^Parameter \#2 \$node of method PHPStan\\BetterReflection\\SourceLocator\\Ast\\Strategy\\NodeToReflection\:\:__invoke\(\) expects PhpParser\\Node\\Expr\\ArrowFunction\|PhpParser\\Node\\Expr\\Closure\|PhpParser\\Node\\Expr\\FuncCall\|PhpParser\\Node\\Stmt\\Class_\|PhpParser\\Node\\Stmt\\Const_\|PhpParser\\Node\\Stmt\\Enum_\|PhpParser\\Node\\Stmt\\Function_\|PhpParser\\Node\\Stmt\\Interface_\|PhpParser\\Node\\Stmt\\Trait_, PhpParser\\Node\\Stmt\\ClassLike given\.$#' + identifier: argument.type count: 2 path: src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php - - message: "#^Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\\. Use objects retrieved from ReflectionProvider instead\\.$#" + message: '#^Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 path: src/Reflection/BetterReflection/SourceLocator/ReflectionClassSourceLocator.php - - message: "#^Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\\. Use objects retrieved from ReflectionProvider instead\\.$#" + message: '#^Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 path: src/Reflection/BetterReflection/SourceLocator/RewriteClassAliasSourceLocator.php - - message: "#^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\\.$#" + message: '#^Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 path: src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericObjectType is error\\-prone and deprecated\\.$#" - count: 3 + message: ''' + #^Call to deprecated method isSubclassOf\(\) of class PHPStan\\Reflection\\ClassReflection\: + Use isSubclassOfClass instead\.$# + ''' + identifier: method.deprecated + count: 1 path: src/Reflection/ClassReflection.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" - count: 1 + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 path: src/Reflection/ClassReflection.php - - message: "#^Method PHPStan\\\\Reflection\\\\ClassReflection\\:\\:getCacheKey\\(\\) should return string but returns string\\|null\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Reflection/ClassReflection.php - - message: "#^Binary operation \"&\" between bool\\|float\\|int\\|string\\|null and bool\\|float\\|int\\|string\\|null results in an error\\.$#" + message: '#^Method PHPStan\\Reflection\\ClassReflection\:\:getCacheKey\(\) should return string but returns string\|null\.$#' + identifier: return.type count: 1 - path: src/Reflection/InitializerExprTypeResolver.php + path: src/Reflection/ClassReflection.php - - message: "#^Binary operation \"\\*\" between bool\\|float\\|int\\|string\\|null and bool\\|float\\|int\\|string\\|null results in an error\\.$#" + message: '#^Binary operation "&" between bool\|float\|int\|string\|null and bool\|float\|int\|string\|null results in an error\.$#' + identifier: binaryOp.invalid count: 1 path: src/Reflection/InitializerExprTypeResolver.php - - message: "#^Binary operation \"\\+\" between bool\\|float\\|int\\|string\\|null and bool\\|float\\|int\\|string\\|null results in an error\\.$#" + message: '#^Binary operation "\*" between bool\|float\|int\|string\|null and bool\|float\|int\|string\|null results in an error\.$#' + identifier: binaryOp.invalid count: 1 path: src/Reflection/InitializerExprTypeResolver.php - - message: "#^Binary operation \"\\-\" between bool\\|float\\|int\\|string\\|null and bool\\|float\\|int\\|string\\|null results in an error\\.$#" + message: '#^Binary operation "\+" between bool\|float\|int\|string\|null and bool\|float\|int\|string\|null results in an error\.$#' + identifier: binaryOp.invalid count: 1 path: src/Reflection/InitializerExprTypeResolver.php - - message: "#^Binary operation \"\\^\" between bool\\|float\\|int\\|string\\|null and bool\\|float\\|int\\|string\\|null results in an error\\.$#" + message: '#^Binary operation "\-" between bool\|float\|int\|string\|null and bool\|float\|int\|string\|null results in an error\.$#' + identifier: binaryOp.invalid count: 1 path: src/Reflection/InitializerExprTypeResolver.php - - message: "#^Binary operation \"\\|\" between bool\\|float\\|int\\|string\\|null and bool\\|float\\|int\\|string\\|null results in an error\\.$#" + message: '#^Binary operation "\^" between bool\|float\|int\|string\|null and bool\|float\|int\|string\|null results in an error\.$#' + identifier: binaryOp.invalid count: 1 path: src/Reflection/InitializerExprTypeResolver.php - - message: """ - #^Call to deprecated method getTypeFromValue\\(\\) of class PHPStan\\\\Type\\\\ConstantTypeHelper\\: - Use PHPStan\\\\Reflection\\\\InitializerExprTypeResolver$# - """ + message: '#^Binary operation "\|" between bool\|float\|int\|string\|null and bool\|float\|int\|string\|null results in an error\.$#' + identifier: binaryOp.invalid count: 1 path: src/Reflection/InitializerExprTypeResolver.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 22 path: src/Reflection/InitializerExprTypeResolver.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 4 path: src/Reflection/InitializerExprTypeResolver.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Reflection/InitializerExprTypeResolver.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 10 path: src/Reflection/InitializerExprTypeResolver.php - - message: "#^PHPDoc tag @var with type float\\|int is not subtype of native type int\\.$#" + message: '#^PHPDoc tag @var with type float\|int is not subtype of native type int\.$#' + identifier: varTag.nativeType count: 1 path: src/Reflection/InitializerExprTypeResolver.php - - message: "#^PHPDoc tag @var with type float\\|int is not subtype of type int\\.$#" + message: '#^PHPDoc tag @var with type float\|int is not subtype of type int\.$#' + identifier: varTag.type count: 4 path: src/Reflection/InitializerExprTypeResolver.php - - message: "#^PHPDoc tag @var with type float\\|int\\|null is not subtype of type int\\|null\\.$#" + message: '#^PHPDoc tag @var with type float\|int\|null is not subtype of type int\|null\.$#' + identifier: varTag.type count: 6 path: src/Reflection/InitializerExprTypeResolver.php - - message: "#^Creating new PHPStan\\\\Php8StubsMap is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Creating new PHPStan\\Php8StubsMap is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.constructor count: 1 path: src/Reflection/SignatureMap/Php8SignatureMapProvider.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" - count: 1 - path: src/Rules/Api/NodeConnectingVisitorAttributesRule.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" - count: 1 - path: src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Rules/Classes/ImpossibleInstanceOfRule.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" - count: 2 - path: src/Rules/Classes/RequireExtendsRule.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Rules/Classes/RequireImplementsRule.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 6 path: src/Rules/Comparison/BooleanAndConstantConditionRule.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Rules/Comparison/BooleanNotConstantConditionRule.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 6 path: src/Rules/Comparison/BooleanOrConstantConditionRule.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" - count: 2 - path: src/Rules/Comparison/ConstantLooseComparisonRule.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Rules/Comparison/DoWhileLoopConstantConditionRule.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Rules/Comparison/ElseIfConstantConditionRule.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Rules/Comparison/IfConstantConditionRule.php - - message: """ - #^Call to deprecated method doNotTreatPhpDocTypesAsCertain\\(\\) of interface PHPStan\\\\Analyser\\\\Scope\\: - Use getNativeType\\(\\)$# - """ - count: 1 - path: src/Rules/Comparison/ImpossibleCheckTypeHelper.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Rules/Comparison/ImpossibleCheckTypeHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" - count: 2 + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 path: src/Rules/Comparison/ImpossibleCheckTypeHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Rules/Comparison/ImpossibleCheckTypeHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\TypeWithClassName is error\\-prone and deprecated\\. Use Type\\:\\:getObjectClassNames\\(\\) or Type\\:\\:getObjectClassReflections\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\TypeWithClassName is error\-prone and deprecated\. Use Type\:\:getObjectClassNames\(\) or Type\:\:getObjectClassReflections\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Rules/Comparison/ImpossibleCheckTypeHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 4 path: src/Rules/Comparison/LogicalXorConstantConditionRule.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 4 path: src/Rules/Comparison/MatchExpressionRule.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Rules/Comparison/TernaryOperatorConstantConditionRule.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" - count: 1 - path: src/Rules/Comparison/UnreachableIfBranchesRule.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" - count: 1 - path: src/Rules/Comparison/UnreachableTernaryElseBranchRule.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php - - message: "#^Function class_implements\\(\\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\\. Use objects retrieved from ReflectionProvider instead\\.$#" + message: '#^Function class_implements\(\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 path: src/Rules/DirectRegistry.php - - message: "#^Function class_parents\\(\\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\\. Use objects retrieved from ReflectionProvider instead\\.$#" + message: '#^Function class_parents\(\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 path: src/Rules/DirectRegistry.php - - message: "#^Method PHPStan\\\\Rules\\\\DirectRegistry\\:\\:__construct\\(\\) has parameter \\$rules with generic interface PHPStan\\\\Rules\\\\Rule but does not specify its types\\: TNodeType$#" + message: '#^Method PHPStan\\Rules\\DirectRegistry\:\:__construct\(\) has parameter \$rules with generic interface PHPStan\\Rules\\Rule but does not specify its types\: TNodeType$#' + identifier: missingType.generics count: 1 path: src/Rules/DirectRegistry.php - - message: "#^Property PHPStan\\\\Rules\\\\DirectRegistry\\:\\:\\$cache with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: '#^Property PHPStan\\Rules\\DirectRegistry\:\:\$cache with generic interface PHPStan\\Rules\\Rule does not specify its types\: TNodeType$#' + identifier: missingType.generics count: 1 path: src/Rules/DirectRegistry.php - - message: "#^Property PHPStan\\\\Rules\\\\DirectRegistry\\:\\:\\$rules with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: '#^Property PHPStan\\Rules\\DirectRegistry\:\:\$rules with generic interface PHPStan\\Rules\\Rule does not specify its types\: TNodeType$#' + identifier: missingType.generics count: 1 path: src/Rules/DirectRegistry.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericObjectType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Rules/Generics/GenericAncestorsCheck.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Rules/Generics/TemplateTypeCheck.php - - message: "#^Function class_implements\\(\\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\\. Use objects retrieved from ReflectionProvider instead\\.$#" + message: '#^Function class_implements\(\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 path: src/Rules/LazyRegistry.php - - message: "#^Function class_parents\\(\\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\\. Use objects retrieved from ReflectionProvider instead\\.$#" + message: '#^Function class_parents\(\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 path: src/Rules/LazyRegistry.php - - message: "#^Method PHPStan\\\\Rules\\\\LazyRegistry\\:\\:getRulesFromContainer\\(\\) return type with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: '#^Method PHPStan\\Rules\\LazyRegistry\:\:getRulesFromContainer\(\) return type with generic interface PHPStan\\Rules\\Rule does not specify its types\: TNodeType$#' + identifier: missingType.generics count: 1 path: src/Rules/LazyRegistry.php - - message: "#^Property PHPStan\\\\Rules\\\\LazyRegistry\\:\\:\\$cache with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: '#^Property PHPStan\\Rules\\LazyRegistry\:\:\$cache with generic interface PHPStan\\Rules\\Rule does not specify its types\: TNodeType$#' + identifier: missingType.generics count: 1 path: src/Rules/LazyRegistry.php - - message: "#^Property PHPStan\\\\Rules\\\\LazyRegistry\\:\\:\\$rules with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: '#^Property PHPStan\\Rules\\LazyRegistry\:\:\$rules with generic interface PHPStan\\Rules\\Rule does not specify its types\: TNodeType$#' + identifier: missingType.generics count: 1 path: src/Rules/LazyRegistry.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Rules/Methods/MethodParameterComparisonHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IterableType is error\\-prone and deprecated\\. Use Type\\:\\:isIterable\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Rules/Methods/MethodParameterComparisonHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) and Type\\:\\:getClassStringObjectType\\(\\) instead\\.$#" - count: 1 - path: src/Rules/Methods/StaticMethodCallCheck.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IterableType is error\-prone and deprecated\. Use Type\:\:isIterable\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Rules/PhpDoc/RequireExtendsCheck.php + path: src/Rules/Methods/MethodParameterComparisonHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) and Type\:\:getClassStringObjectType\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php + path: src/Rules/Methods/StaticMethodCallCheck.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericObjectType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Rules/PhpDoc/VarTagTypeRuleHelper.php - - message: "#^Access to an undefined property T of PHPStan\\\\Rules\\\\RuleError\\:\\:\\$tip\\.$#" + message: '#^Access to an undefined property T of PHPStan\\Rules\\RuleError\:\:\$tip\.$#' + identifier: property.notFound count: 2 path: src/Rules/RuleErrorBuilder.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" - count: 1 - path: src/Rules/RuleLevelHelper.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\NullType is error\\-prone and deprecated\\. Use Type\\:\\:isNull\\(\\) instead\\.$#" - count: 2 - path: src/Rules/RuleLevelHelper.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectWithoutClassType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Rules/RuleLevelHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" - count: 1 - path: src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Call to function method_exists\(\) with ''PHPUnit\\\\Framework\\\\TestCase'' and ''assertFileDoesNotEx…'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: src/Rules/UnusedFunctionParametersCheck.php + path: src/Testing/LevelsTestCase.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" - count: 1 - path: src/Rules/Variables/CompactVariablesRule.php + message: '#^Catching internal class PHPUnit\\Framework\\AssertionFailedError\.$#' + identifier: catch.internalClass + count: 2 + path: src/Testing/LevelsTestCase.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Return type of method PHPStan\\Testing\\LevelsTestCase\:\:compareFiles\(\) has typehint with internal class PHPUnit\\Framework\\AssertionFailedError\.$#' + identifier: return.internalClass count: 1 - path: src/Rules/Variables/CompactVariablesRule.php + path: src/Testing/LevelsTestCase.php - - message: "#^Anonymous function has an unused use \\$container\\.$#" + message: '#^Anonymous function has an unused use \$container\.$#' + identifier: closure.unusedUse count: 1 path: src/Testing/PHPStanTestCase.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Testing/TypeInferenceTestCase.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Accessory/AccessoryArrayListType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Accessory/AccessoryLiteralStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Accessory/AccessoryLowercaseStringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Accessory/AccessoryNonEmptyStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Accessory/AccessoryNonEmptyStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Accessory/AccessoryNonFalsyStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Accessory/AccessoryNumericStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Accessory/AccessoryNumericStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Accessory/AccessoryUppercaseStringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Accessory/HasMethodType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Accessory/HasOffsetType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/Accessory/HasOffsetValueType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/Accessory/HasOffsetValueType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Accessory/HasOffsetValueType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Accessory/HasPropertyType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Accessory/NonEmptyArrayType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Accessory/OversizedArrayType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/ArrayType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" - count: 1 + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 path: src/Type/ArrayType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/ArrayType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/ArrayType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\BooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isBoolean\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\BooleanType is error\-prone and deprecated\. Use Type\:\:isBoolean\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/BooleanType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/BooleanType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\CallableType is error\\-prone and deprecated\\. Use Type\\:\\:isCallable\\(\\) and Type\\:\\:getCallableParametersAcceptors\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\CallableType is error\-prone and deprecated\. Use Type\:\:isCallable\(\) and Type\:\:getCallableParametersAcceptors\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 4 path: src/Type/CallableType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/CallableType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/ClosureType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Constant/ConstantArrayType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/Constant/ConstantArrayType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" - count: 8 + message: '#^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 - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" - count: 3 + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 path: src/Type/Constant/ConstantArrayType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/Constant/ConstantArrayType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/Constant/ConstantArrayTypeBuilder.php - - message: "#^PHPDoc tag @var assumes the expression with type PHPStan\\\\Type\\\\Type is always PHPStan\\\\Type\\\\Constant\\\\ConstantIntegerType\\|PHPStan\\\\Type\\\\Constant\\\\ConstantStringType but it's error\\-prone and dangerous\\.$#" + message: '#^PHPDoc tag @var assumes the expression with type PHPStan\\Type\\Type is always PHPStan\\Type\\Constant\\ConstantIntegerType\|PHPStan\\Type\\Constant\\ConstantStringType but it''s error\-prone and dangerous\.$#' + identifier: phpstanApi.varTagAssumption count: 2 path: src/Type/Constant/ConstantArrayTypeBuilder.php - - message: "#^PHPDoc tag @var with type float\\|int is not subtype of native type int\\.$#" + message: '#^PHPDoc tag @var with type float\|int is not subtype of native type int\.$#' + identifier: varTag.nativeType count: 2 path: src/Type/Constant/ConstantArrayTypeBuilder.php - - message: "#^PHPDoc tag @var with type float\\|int is not subtype of type int\\.$#" + message: '#^PHPDoc tag @var with type float\|int is not subtype of type int\.$#' + identifier: varTag.type count: 1 path: src/Type/Constant/ConstantArrayTypeBuilder.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\BooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isBoolean\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\BooleanType is error\-prone and deprecated\. Use Type\:\:isBoolean\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Constant/ConstantBooleanType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 4 path: src/Type/Constant/ConstantBooleanType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/Constant/ConstantBooleanType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 4 path: src/Type/Constant/ConstantFloatType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\FloatType is error\\-prone and deprecated\\. Use Type\\:\\:isFloat\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\FloatType is error\-prone and deprecated\. Use Type\:\:isFloat\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Constant/ConstantFloatType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 4 path: src/Type/Constant/ConstantIntegerType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntegerType is error\\-prone and deprecated\\. Use Type\\:\\:isInteger\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntegerType is error\-prone and deprecated\. Use Type\:\:isInteger\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Constant/ConstantIntegerType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Constant/ConstantStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 4 path: src/Type/Constant/ConstantStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 4 path: src/Type/Constant/ConstantStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) and Type\\:\\:getClassStringObjectType\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) and Type\:\:getClassStringObjectType\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Constant/ConstantStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\StringType is error\\-prone and deprecated\\. Use Type\\:\\:isString\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\StringType is error\-prone and deprecated\. Use Type\:\:isString\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Constant/ConstantStringType.php - - message: "#^PHPDoc tag @var with type int\\|string is not subtype of type string\\.$#" + message: '#^PHPDoc tag @var with type int\|string is not subtype of type string\.$#' + identifier: varTag.type count: 1 path: src/Type/Constant/ConstantStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Constant/OversizedArrayBuilder.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Enum\\\\EnumCaseObjectType is error\\-prone and deprecated\\. Use Type\\:\\:getEnumCases\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Enum\\EnumCaseObjectType is error\-prone and deprecated\. Use Type\:\:getEnumCases\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/Enum/EnumCaseObjectType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/ExponentiateHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericObjectType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/FileTypeMapper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\FloatType is error\\-prone and deprecated\\. Use Type\\:\\:isFloat\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\FloatType is error\-prone and deprecated\. Use Type\:\:isFloat\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/FloatType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Generic/GenericClassStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 4 path: src/Type/Generic/GenericClassStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) and Type\\:\\:getClassStringObjectType\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) and Type\:\:getClassStringObjectType\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 4 path: src/Type/Generic/GenericClassStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Generic/GenericClassStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\StringType is error\\-prone and deprecated\\. Use Type\\:\\:isString\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\StringType is error\-prone and deprecated\. Use Type\:\:isString\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/Generic/GenericClassStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericObjectType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/GenericObjectType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Generic/GenericObjectType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Generic/GenericObjectType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\TypeWithClassName is error\\-prone and deprecated\\. Use Type\\:\\:getObjectClassNames\\(\\) or Type\\:\\:getObjectClassReflections\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\TypeWithClassName is error\-prone and deprecated\. Use Type\:\:getObjectClassNames\(\) or Type\:\:getObjectClassReflections\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/Generic/GenericObjectType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/GenericStaticType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\TypeWithClassName is error\-prone and deprecated\. Use Type\:\:getObjectClassNames\(\) or Type\:\:getObjectClassReflections\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/GenericStaticType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateArrayType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateBenevolentUnionType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateBooleanType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateConstantArrayType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateConstantIntegerType.php - - message: "#^Method PHPStan\\\\Type\\\\Generic\\\\TemplateConstantIntegerType\\:\\:toPhpDocNode\\(\\) should return PHPStan\\\\PhpDocParser\\\\Ast\\\\Type\\\\ConstTypeNode but returns PHPStan\\\\PhpDocParser\\\\Ast\\\\Type\\\\IdentifierTypeNode\\.$#" + message: '#^Method PHPStan\\Type\\Generic\\TemplateConstantIntegerType\:\:toPhpDocNode\(\) should return PHPStan\\PhpDocParser\\Ast\\Type\\ConstTypeNode but returns PHPStan\\PhpDocParser\\Ast\\Type\\IdentifierTypeNode\.$#' + identifier: return.type count: 1 path: src/Type/Generic/TemplateConstantIntegerType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateConstantStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateFloatType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateGenericObjectType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateIntegerType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateIntersectionType.php - - message: "#^Instanceof between PHPStan\\\\Type\\\\Type and PHPStan\\\\Type\\\\IntersectionType will always evaluate to false\\.$#" - count: 2 - path: src/Type/Generic/TemplateIntersectionType.php + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateIterableType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateKeyOfType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/Generic/TemplateMixedType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateObjectShapeType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateObjectType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateObjectWithoutClassType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/Generic/TemplateStrictMixedType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Generic/TemplateTypeFactory.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\BooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isBoolean\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\BooleanType is error\-prone and deprecated\. Use Type\:\:isBoolean\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Generic/TemplateTypeFactory.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Generic/TemplateTypeFactory.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Generic/TemplateTypeFactory.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\FloatType is error\\-prone and deprecated\\. Use Type\\:\\:isFloat\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\FloatType is error\-prone and deprecated\. Use Type\:\:isFloat\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Generic/TemplateTypeFactory.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericObjectType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Generic/TemplateTypeFactory.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntegerType is error\\-prone and deprecated\\. Use Type\\:\\:isInteger\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntegerType is error\-prone and deprecated\. Use Type\:\:isInteger\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Generic/TemplateTypeFactory.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Generic/TemplateTypeFactory.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectShapeType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) and Type\\:\\:hasProperty\\(\\) instead\\.$#" + message: '#^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 - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ObjectShapeType is error\-prone and deprecated\. Use Type\:\:isObject\(\) and Type\:\:hasProperty\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Generic/TemplateTypeFactory.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectWithoutClassType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Generic/TemplateTypeFactory.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\StringType is error\\-prone and deprecated\\. Use Type\\:\\:isString\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ObjectWithoutClassType is error\-prone and deprecated\. Use Type\:\:isObject\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Generic/TemplateTypeFactory.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" - count: 3 - path: src/Type/Generic/TemplateUnionType.php + message: '#^Doing instanceof PHPStan\\Type\\StringType is error\-prone and deprecated\. Use Type\:\:isString\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php - - message: "#^Instanceof between PHPStan\\\\Type\\\\Type and PHPStan\\\\Type\\\\UnionType will always evaluate to false\\.$#" - count: 2 + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 path: src/Type/Generic/TemplateUnionType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/IntegerRangeType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntegerType is error\\-prone and deprecated\\. Use Type\\:\\:isInteger\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntegerType is error\-prone and deprecated\. Use Type\:\:isInteger\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/IntegerRangeType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/IntegerRangeType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntegerType is error\\-prone and deprecated\\. Use Type\\:\\:isInteger\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntegerType is error\-prone and deprecated\. Use Type\:\:isInteger\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/IntegerType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Accessory\\\\AccessoryType is error\\-prone and deprecated\\. Use methods on PHPStan\\\\Type\\\\Type instead\\.$#" - count: 3 + message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 path: src/Type/IntersectionType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\BooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isBoolean\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\BooleanType is error\-prone and deprecated\. Use Type\:\:isBoolean\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/IntersectionType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\CallableType is error\\-prone and deprecated\\. Use Type\\:\\:isCallable\\(\\) and Type\\:\\:getCallableParametersAcceptors\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\CallableType is error\-prone and deprecated\. Use Type\:\:isCallable\(\) and Type\:\:getCallableParametersAcceptors\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/IntersectionType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/IntersectionType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/IntersectionType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 4 path: src/Type/IntersectionType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/IterableType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IterableType is error\\-prone and deprecated\\. Use Type\\:\\:isIterable\\(\\) instead\\.$#" - count: 2 + message: '#^Doing instanceof PHPStan\\Type\\IterableType is error\-prone and deprecated\. Use Type\:\:isIterable\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 path: src/Type/IterableType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/NullType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\NullType is error\\-prone and deprecated\\. Use Type\\:\\:isNull\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\NullType is error\-prone and deprecated\. Use Type\:\:isNull\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/NullType.php - - message: """ - #^Call to deprecated method getInstance\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Use PHPStan\\\\Reflection\\\\ReflectionProviderStaticAccessor instead$# - """ - count: 2 - path: src/Type/ObjectShapeType.php - - - - message: """ - #^Call to deprecated method getUniversalObjectCratesClasses\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Inject %%universalObjectCratesClasses%% parameter instead\\.$# - """ - count: 2 - path: src/Type/ObjectShapeType.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/ObjectShapeType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectShapeType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) and Type\\:\\:hasProperty\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ObjectShapeType is error\-prone and deprecated\. Use Type\:\:isObject\(\) and Type\:\:hasProperty\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/ObjectShapeType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectWithoutClassType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ObjectWithoutClassType is error\-prone and deprecated\. Use Type\:\:isObject\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/ObjectShapeType.php - - message: """ - #^Call to deprecated method getInstance\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Use PHPStan\\\\Reflection\\\\ReflectionProviderStaticAccessor instead$# - """ - count: 1 - path: src/Type/ObjectType.php - - - - message: """ - #^Call to deprecated method getUniversalObjectCratesClasses\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Inject %%universalObjectCratesClasses%% parameter instead\\.$# - """ + message: '#^Doing instanceof PHPStan\\Type\\Enum\\EnumCaseObjectType is error\-prone and deprecated\. Use Type\:\:getEnumCases\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/ObjectType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Enum\\\\EnumCaseObjectType is error\\-prone and deprecated\\. Use Type\\:\\:getEnumCases\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/ObjectType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericObjectType is error\\-prone and deprecated\\.$#" - count: 1 - path: src/Type/ObjectType.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" - count: 6 + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 3 path: src/Type/ObjectType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectWithoutClassType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ObjectWithoutClassType is error\-prone and deprecated\. Use Type\:\:isObject\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/ObjectType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectShapeType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) and Type\\:\\:hasProperty\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ObjectShapeType is error\-prone and deprecated\. Use Type\:\:isObject\(\) and Type\:\:hasProperty\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/ObjectWithoutClassType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectWithoutClassType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ObjectWithoutClassType is error\-prone and deprecated\. Use Type\:\:isObject\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 4 path: src/Type/ObjectWithoutClassType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 - path: src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php + path: src/Type/PHPStan/ClassNameUsageLocationCreateIdentifierDynamicReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" - count: 1 - path: src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" - count: 4 - path: src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" - count: 1 - path: src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 16 path: src/Type/Php/BcMathStringOrNullReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/CompactFunctionReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/CompactFunctionReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/DefineConstantTypeSpecifyingExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/DefinedConstantTypeSpecifyingExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\TypeWithClassName is error\\-prone and deprecated\\. Use Type\\:\\:getObjectClassNames\\(\\) or Type\\:\\:getObjectClassReflections\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\TypeWithClassName is error\-prone and deprecated\. Use Type\:\:getObjectClassNames\(\) or Type\:\:getObjectClassReflections\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/DsMapDynamicReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/Php/FilterFunctionReturnTypeHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/FilterFunctionReturnTypeHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" - count: 1 - path: src/Type/Php/ImplodeFunctionReturnTypeExtension.php - - - - message: """ - #^Call to deprecated method getConstantScalars\\(\\) of class PHPStan\\\\Type\\\\TypeUtils\\: - Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\)$# - """ - count: 2 - path: src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php - - - - message: """ - #^Call to deprecated method getEnumCaseObjects\\(\\) of class PHPStan\\\\Type\\\\TypeUtils\\: - Use Type\\:\\:getEnumCases\\(\\)$# - """ - count: 2 - path: src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/IsAFunctionTypeSpecifyingExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) and Type\\:\\:getClassStringObjectType\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) and Type\:\:getClassStringObjectType\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php - - message: """ - #^Call to deprecated method getTypeFromValue\\(\\) of class PHPStan\\\\Type\\\\ConstantTypeHelper\\: - Use PHPStan\\\\Reflection\\\\InitializerExprTypeResolver$# - """ + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" - count: 1 - path: src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" - count: 2 - path: src/Type/Php/LtrimFunctionReturnTypeExtension.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" - count: 1 - path: src/Type/Php/MbStrlenFunctionReturnTypeExtension.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/MethodExistsTypeSpecifyingExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" - count: 4 - path: src/Type/Php/MinMaxFunctionReturnTypeExtension.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantValue\\(\\) or Type\\:\\:generalize\\(\\) instead\\.$#" - count: 1 + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 path: src/Type/Php/MinMaxFunctionReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/Php/MinMaxFunctionReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/Php/PropertyExistsTypeSpecifyingExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/Php/RangeFunctionReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" - count: 1 - path: src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) and Type\\:\\:getClassStringObjectType\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) and Type\:\:getClassStringObjectType\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php - - message: "#^Cannot access offset int\\<0, max\\> on \\(float\\|int\\)\\.$#" + message: '#^Cannot access offset int\<0, max\> on \(float\|int\)\.$#' + identifier: offsetAccess.nonOffsetAccessible count: 2 path: src/Type/Php/StrIncrementDecrementFunctionReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Php/StrRepeatFunctionReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" - count: 1 - path: src/Type/Php/StrlenFunctionReturnTypeExtension.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/StaticType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectWithoutClassType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ObjectWithoutClassType is error\-prone and deprecated\. Use Type\:\:isObject\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/StaticType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/StringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\StringType is error\\-prone and deprecated\\. Use Type\\:\\:isString\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\StringType is error\-prone and deprecated\. Use Type\:\:isString\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/StringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Accessory\\\\AccessoryType is error\\-prone and deprecated\\. Use methods on PHPStan\\\\Type\\\\Type instead\\.$#" - count: 1 - path: src/Type/TypeCombinator.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 5 path: src/Type/TypeCombinator.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\BooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isBoolean\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\BooleanType is error\-prone and deprecated\. Use Type\:\:isBoolean\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/TypeCombinator.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\CallableType is error\\-prone and deprecated\\. Use Type\\:\\:isCallable\\(\\) and Type\\:\\:getCallableParametersAcceptors\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\CallableType is error\-prone and deprecated\. Use Type\:\:isCallable\(\) and Type\:\:getCallableParametersAcceptors\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/TypeCombinator.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/TypeCombinator.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/TypeCombinator.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" - count: 10 + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 16 path: src/Type/TypeCombinator.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 5 path: src/Type/TypeCombinator.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\FloatType is error\\-prone and deprecated\\. Use Type\\:\\:isFloat\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\FloatType is error\-prone and deprecated\. Use Type\:\:isFloat\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/TypeCombinator.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) and Type\\:\\:getClassStringObjectType\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) and Type\:\:getClassStringObjectType\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/TypeCombinator.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntegerType is error\\-prone and deprecated\\. Use Type\\:\\:isInteger\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntegerType is error\-prone and deprecated\. Use Type\:\:isInteger\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/TypeCombinator.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/TypeCombinator.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IterableType is error\\-prone and deprecated\\. Use Type\\:\\:isIterable\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IterableType is error\-prone and deprecated\. Use Type\:\:isIterable\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 8 path: src/Type/TypeCombinator.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\NullType is error\\-prone and deprecated\\. Use Type\\:\\:isNull\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\NullType is error\-prone and deprecated\. Use Type\:\:isNull\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/TypeCombinator.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectShapeType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) and Type\\:\\:hasProperty\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ObjectShapeType is error\-prone and deprecated\. Use Type\:\:isObject\(\) and Type\:\:hasProperty\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/TypeCombinator.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\StringType is error\\-prone and deprecated\\. Use Type\\:\\:isString\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\StringType is error\-prone and deprecated\. Use Type\:\:isString\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/TypeCombinator.php - - message: "#^Instanceof between PHPStan\\\\Type\\\\Constant\\\\ConstantIntegerType and PHPStan\\\\Type\\\\Constant\\\\ConstantIntegerType will always evaluate to true\\.$#" + message: '#^Instanceof between PHPStan\\Type\\Constant\\ConstantIntegerType and PHPStan\\Type\\Constant\\ConstantIntegerType will always evaluate to true\.$#' + identifier: instanceof.alwaysTrue count: 1 path: src/Type/TypeCombinator.php - - message: "#^Result of \\|\\| is always true\\.$#" + message: '#^Result of \|\| is always true\.$#' + identifier: booleanOr.alwaysTrue count: 1 path: src/Type/TypeCombinator.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" - count: 3 - path: src/Type/TypeUtils.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" - count: 5 + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 path: src/Type/TypeUtils.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" - count: 5 + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 path: src/Type/TypeUtils.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\TypeWithClassName is error\\-prone and deprecated\\. Use Type\\:\\:getObjectClassNames\\(\\) or Type\\:\\:getObjectClassReflections\\(\\) instead\\.$#" - count: 1 - path: src/Type/TypeUtils.php + message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/TypehintHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 3 path: src/Type/TypehintHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IterableType is error\\-prone and deprecated\\. Use Type\\:\\:isIterable\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IterableType is error\-prone and deprecated\. Use Type\:\:isIterable\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/TypehintHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\CallableType is error\\-prone and deprecated\\. Use Type\\:\\:isCallable\\(\\) and Type\\:\\:getCallableParametersAcceptors\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\CallableType is error\-prone and deprecated\. Use Type\:\:isCallable\(\) and Type\:\:getCallableParametersAcceptors\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/UnionType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) and Type\\:\\:getClassStringObjectType\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) and Type\:\:getClassStringObjectType\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/UnionType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/UnionType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IterableType is error\\-prone and deprecated\\. Use Type\\:\\:isIterable\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IterableType is error\-prone and deprecated\. Use Type\:\:isIterable\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/UnionType.php - - message: "#^PHPDoc tag @var assumes the expression with type PHPStan\\\\Type\\\\Type is always PHPStan\\\\Type\\\\BooleanType but it's error\\-prone and dangerous\\.$#" + message: '#^PHPDoc tag @var assumes the expression with type PHPStan\\Type\\Type is always PHPStan\\Type\\BooleanType but it''s error\-prone and dangerous\.$#' + identifier: phpstanApi.varTagAssumption count: 1 path: src/Type/UnionType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Accessory\\\\AccessoryType is error\\-prone and deprecated\\. Use methods on PHPStan\\\\Type\\\\Type instead\\.$#" - count: 3 - path: src/Type/UnionTypeHelper.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\CallableType is error\\-prone and deprecated\\. Use Type\\:\\:isCallable\\(\\) and Type\\:\\:getCallableParametersAcceptors\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\CallableType is error\-prone and deprecated\. Use Type\:\:isCallable\(\) and Type\:\:getCallableParametersAcceptors\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/UnionTypeHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 4 path: src/Type/UnionTypeHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/UnionTypeHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/UnionTypeHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntegerType is error\\-prone and deprecated\\. Use Type\\:\\:isInteger\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntegerType is error\-prone and deprecated\. Use Type\:\:isInteger\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/UnionTypeHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\NullType is error\\-prone and deprecated\\. Use Type\\:\\:isNull\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\NullType is error\-prone and deprecated\. Use Type\:\:isNull\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/UnionTypeHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\VoidType is error\\-prone and deprecated\\. Use Type\\:\\:isVoid\\(\\) instead\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\VoidType is error\-prone and deprecated\. Use Type\:\:isVoid\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 path: src/Type/VoidType.php - - message: "#^Unreachable statement \\- code above always terminates\\.$#" + message: '#^Unreachable statement \- code above always terminates\.$#' + identifier: deadCode.unreachable count: 1 path: tests/PHPStan/Analyser/AnalyserTest.php - - message: "#^Class PHPStan\\\\Analyser\\\\AnonymousClassNameRuleTest extends generic class PHPStan\\\\Testing\\\\RuleTestCase but does not specify its types\\: TRule$#" + message: '#^Class PHPStan\\Analyser\\AnonymousClassNameRuleTest extends generic class PHPStan\\Testing\\RuleTestCase but does not specify its types\: TRule$#' + identifier: missingType.generics count: 1 path: tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php - - message: "#^Method PHPStan\\\\Analyser\\\\AnonymousClassNameRuleTest\\:\\:getRule\\(\\) return type with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: '#^Method PHPStan\\Analyser\\AnonymousClassNameRuleTest\:\:getRule\(\) return type with generic interface PHPStan\\Rules\\Rule does not specify its types\: TNodeType$#' + identifier: missingType.generics count: 1 path: tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php - - message: "#^Class PHPStan\\\\Analyser\\\\EvaluationOrderTest extends generic class PHPStan\\\\Testing\\\\RuleTestCase but does not specify its types\\: TRule$#" + message: '#^Class PHPStan\\Analyser\\EvaluationOrderTest extends generic class PHPStan\\Testing\\RuleTestCase but does not specify its types\: TRule$#' + identifier: missingType.generics count: 1 path: tests/PHPStan/Analyser/EvaluationOrderTest.php - - message: "#^Method PHPStan\\\\Analyser\\\\EvaluationOrderTest\\:\\:getRule\\(\\) return type with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: '#^Method PHPStan\\Analyser\\EvaluationOrderTest\:\:getRule\(\) return type with generic interface PHPStan\\Rules\\Rule does not specify its types\: TNodeType$#' + identifier: missingType.generics count: 1 path: tests/PHPStan/Analyser/EvaluationOrderTest.php - - message: "#^Constant SOME_CONSTANT_IN_AUTOLOAD_FILE not found\\.$#" + message: '#^Constant SOME_CONSTANT_IN_AUTOLOAD_FILE not found\.$#' + identifier: constant.notFound count: 1 path: tests/PHPStan/Command/AnalyseCommandTest.php - - message: "#^Class PHPStan\\\\Node\\\\FileNodeTest extends generic class PHPStan\\\\Testing\\\\RuleTestCase but does not specify its types\\: TRule$#" + message: '#^Class PHPStan\\Node\\FileNodeTest extends generic class PHPStan\\Testing\\RuleTestCase but does not specify its types\: TRule$#' + identifier: missingType.generics count: 1 path: tests/PHPStan/Node/FileNodeTest.php - - message: "#^Method PHPStan\\\\Node\\\\FileNodeTest\\:\\:getRule\\(\\) return type with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: '#^Method PHPStan\\Node\\FileNodeTest\:\:getRule\(\) return type with generic interface PHPStan\\Rules\\Rule does not specify its types\: TNodeType$#' + identifier: missingType.generics count: 1 path: tests/PHPStan/Node/FileNodeTest.php - - message: "#^PHPDoc tag @var with type string is not subtype of type class\\-string\\.$#" + message: '#^Access to constant on internal class InternalAnnotations\\InternalFoo\.$#' + identifier: classConstant.internalClass count: 1 - path: tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php + path: tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php - - message: "#^Creating new PHPStan\\\\Php8StubsMap is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Access to constant on internal interface InternalAnnotations\\InternalFooInterface\.$#' + identifier: classConstant.internalInterface count: 1 - path: tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php - - - - message: "#^Creating new PHPStan\\\\Php8StubsMap is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" - count: 1 - path: tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php - - - - message: """ - #^Instantiation of deprecated class PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayItemTypeRule\\: - Replaced by PHPStan\\\\Rules\\\\Properties\\\\TypesAssignedToPropertiesRule$# - """ - count: 1 - path: tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php + path: tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php - - message: """ - #^Return type of method PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayItemTypeRuleTest\\:\\:getRule\\(\\) has typehint with deprecated class PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayItemTypeRule\\: - Replaced by PHPStan\\\\Rules\\\\Properties\\\\TypesAssignedToPropertiesRule$# - """ + message: '#^Access to constant on internal trait InternalAnnotations\\InternalFooTrait\.$#' + identifier: classConstant.internalTrait count: 1 - path: tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php + path: tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php - - message: """ - #^Instantiation of deprecated class PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayKeyTypeRule\\: - Replaced by PHPStan\\\\Rules\\\\Properties\\\\TypesAssignedToPropertiesRule$# - """ + message: '#^PHPDoc tag @var with type string is not subtype of type class\-string\.$#' + identifier: varTag.type count: 1 - path: tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php - - - - message: """ - #^Return type of method PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayKeyTypeRuleTest\\:\\:getRule\\(\\) has typehint with deprecated class PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayKeyTypeRule\\: - Replaced by PHPStan\\\\Rules\\\\Properties\\\\TypesAssignedToPropertiesRule$# - """ - count: 1 - path: tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php + path: tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php - - message: """ - #^Instantiation of deprecated class PHPStan\\\\Rules\\\\DeadCode\\\\NoopRule\\: - Replaced by PHPStan\\\\Rules\\\\DeadCode\\\\BetterNoopRule$# - """ + message: '#^Creating new PHPStan\\Php8StubsMap is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.constructor count: 1 - path: tests/PHPStan/Rules/DeadCode/NoopRuleTest.php + path: tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php - - message: """ - #^Return type of method PHPStan\\\\Rules\\\\DeadCode\\\\NoopRuleTest\\:\\:getRule\\(\\) has typehint with deprecated class PHPStan\\\\Rules\\\\DeadCode\\\\NoopRule\\: - Replaced by PHPStan\\\\Rules\\\\DeadCode\\\\BetterNoopRule$# - """ + message: '#^Creating new PHPStan\\Php8StubsMap is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.constructor count: 1 - path: tests/PHPStan/Rules/DeadCode/NoopRuleTest.php + path: tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php - - message: """ - #^Instantiation of deprecated class PHPStan\\\\Rules\\\\Functions\\\\ImplodeFunctionRule\\: - Replaced by PHPStan\\\\Rules\\\\Functions\\\\ImplodeParameterCastableToStringRuleTest$# - """ - count: 1 - path: tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php + message: '#^Access to constant on internal class PHPUnit\\Framework\\AssertionFailedError\.$#' + identifier: classConstant.internalClass + count: 2 + path: tests/PHPStan/Testing/TypeInferenceTestCaseTest.php - - message: """ - #^Return type of method PHPStan\\\\Rules\\\\Functions\\\\ImplodeFunctionRuleTest\\:\\:getRule\\(\\) has typehint with deprecated class PHPStan\\\\Rules\\\\Functions\\\\ImplodeFunctionRule\\: - Replaced by PHPStan\\\\Rules\\\\Functions\\\\ImplodeParameterCastableToStringRuleTest$# - """ + message: '#^Catching internal class PHPUnit\\Framework\\AssertionFailedError\.$#' + identifier: catch.internalClass count: 1 - path: tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php + path: tests/PHPStan/Testing/TypeInferenceTestCaseTest.php - - message: "#^PHPDoc tag @var assumes the expression with type PHPStan\\\\Type\\\\Generic\\\\TemplateType is always PHPStan\\\\Type\\\\Generic\\\\TemplateMixedType but it's error\\-prone and dangerous\\.$#" + message: '#^PHPDoc tag @var assumes the expression with type PHPStan\\Type\\Generic\\TemplateType is always PHPStan\\Type\\Generic\\TemplateMixedType but it''s error\-prone and dangerous\.$#' + identifier: phpstanApi.varTagAssumption count: 1 path: tests/PHPStan/Type/IterableTypeTest.php diff --git a/phpunit.xml b/phpunit.xml index b083450096..062728282f 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,41 +1,16 @@ - - - - src - - - - - - - - - - tests/PHPStan - tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php - - - - - - exec - levels - - - + + + + tests/PHPStan + tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php + + + + + exec + levels + + + diff --git a/resources/RegexGrammar.pp b/resources/RegexGrammar.pp index b8bea027d3..3f49912a36 100644 --- a/resources/RegexGrammar.pp +++ b/resources/RegexGrammar.pp @@ -42,14 +42,16 @@ // // 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_literal (?<=[^\\]\[|[^\\]\[\^)\] %token class:_class \] -> default %token class:range \- -%token class:escaped_end_class \\\] // 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]+}) @@ -58,7 +60,8 @@ // Internal options. // See https://www.regular-expressions.info/refmodifiers.html -%token internal_option \(\?([imsxnJUX^]|xx)?-?([imsxnJUX^]|xx)\) +// and https://www.php.net/manual/en/regexp.reference.internal-options.php +%token internal_option \(\?[imsxnJUX^]*-?[imsxnJUX^]+\) // Lookahead and lookbehind assertions. %token lookahead_ \(\?= @@ -88,7 +91,7 @@ %token nc:_named_capturing > -> default %token nc:capturing_name .+?(?=(?) %token non_capturing_ \(\?: -%token non_capturing_internal_option \(\?([imsxnJUX^]|xx)?-?([imsxnJUX^]|xx): +%token non_capturing_internal_option \(\?[imsxnJUX^]*-?[imsxnJUX^]+: %token non_capturing_reset_ \(\?\| %token atomic_group_ \(\?> %token capturing_ \( @@ -132,7 +135,7 @@ alternation() alternation: - concatenation() ( ::alternation:: concatenation() #alternation )* + concatenation()? ( concatenation()? #alternation )* concatenation: ( internal_options() | assertion() | quantification() | condition() ) @@ -151,8 +154,8 @@ | ::assertion_reference_:: alternation() #assertioncondition ) - ::_capturing:: concatenation()? - ( ::alternation:: concatenation()? )? + ::_capturing:: + alternation() ::_capturing:: assertion: @@ -162,7 +165,8 @@ | ::lookbehind_:: #lookbehind | ::negative_lookbehind_:: #negativelookbehind ) - alternation() ::_capturing:: + alternation() + ::_capturing:: quantification: ( class() | simple() ) ( quantifier() #quantification )? @@ -177,10 +181,14 @@ #class: ( - ::negative_class_:: #negativeclass + ::negative_class_fc_:: #negativeclass + <_class> + | ::class_fc_:: + <_class> + | ::negative_class_:: #negativeclass | ::class_:: ) - ( | <_class_literal> )? ( | | range() | literal() | )* ? + ? ( | | range() ? | literal() )* ? ::_class:: #range: @@ -201,7 +209,8 @@ | ::atomic_group_:: #atomicgroup | ::capturing_:: ) - alternation() ::_capturing:: + alternation() + ::_capturing:: non_capturing_internal_options: diff --git a/resources/functionMap.php b/resources/functionMap.php index 558096b86b..1afb4664ec 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -57,9 +57,7 @@ return [ '_' => ['string', 'message'=>'string'], -'abs' => ['0|positive-int', 'number'=>'int'], -'abs\'1' => ['float', 'number'=>'float'], -'abs\'2' => ['float|0|positive-int', 'number'=>'string'], +'abs' => ['float|0|positive-int', 'num'=>'int|float'], 'accelerator_get_configuration' => ['array'], 'accelerator_get_scripts' => ['array'], 'accelerator_get_status' => ['array', 'fetch_scripts'=>'bool'], @@ -69,102 +67,28 @@ 'acosh' => ['float', 'number'=>'float'], 'addcslashes' => ['string', 'str'=>'string', 'charlist'=>'string'], 'addslashes' => ['string', 'str'=>'string'], -'AMQPChannel::__construct' => ['void', 'connection'=>'AMQPConnection'], -'AMQPChannel::basicRecover' => ['void', 'requeue='=>'bool'], -'AMQPChannel::commitTransaction' => ['void'], 'AMQPChannel::getChannelId' => ['int<1, 65535>'], -'AMQPChannel::getConnection' => ['AMQPConnection'], 'AMQPChannel::getPrefetchCount' => ['int<0, 65535>'], 'AMQPChannel::getPrefetchSize' => ['int<0, max>'], -'AMQPChannel::isConnected' => ['bool'], -'AMQPChannel::qos' => ['void', 'size'=>'int', 'count'=>'int', 'global='=>'bool'], -'AMQPChannel::rollbackTransaction' => ['void'], -'AMQPChannel::setPrefetchCount' => ['void', 'count'=>'int'], -'AMQPChannel::setPrefetchSize' => ['void', 'size'=>'int'], -'AMQPChannel::startTransaction' => ['void'], -'AMQPConnection::__construct' => ['void', 'credentials='=>'array'], -'AMQPConnection::connect' => ['void'], -'AMQPConnection::disconnect' => ['void'], -'AMQPConnection::getHost' => ['string'], -'AMQPConnection::getLogin' => ['string'], 'AMQPConnection::getMaxChannels' => ['int<1, 65535>'], -'AMQPConnection::getPassword' => ['string'], 'AMQPConnection::getPort' => ['int<1, 65535>'], -'AMQPConnection::getReadTimeout' => ['float'], -'AMQPConnection::getTimeout' => ['float'], 'AMQPConnection::getUsedChannels' => ['int<1, 65535>'], -'AMQPConnection::getVhost' => ['string'], -'AMQPConnection::getWriteTimeout' => ['float'], -'AMQPConnection::isConnected' => ['bool'], -'AMQPConnection::isPersistent' => ['bool'], -'AMQPConnection::pconnect' => ['void'], -'AMQPConnection::pdisconnect' => ['void'], -'AMQPConnection::preconnect' => ['void'], -'AMQPConnection::reconnect' => ['void'], -'AMQPConnection::setHost' => ['void', 'host'=>'string'], -'AMQPConnection::setLogin' => ['void', 'login'=>'string'], -'AMQPConnection::setPassword' => ['void', 'password'=>'string'], -'AMQPConnection::setPort' => ['void', 'port'=>'int'], -'AMQPConnection::setReadTimeout' => ['void', 'timeout'=>'int'], -'AMQPConnection::setTimeout' => ['void', 'timeout'=>'int'], -'AMQPConnection::setVhost' => ['void', 'vhost'=>'string'], -'AMQPConnection::setWriteTimeout' => ['void', 'timeout'=>'int'], -'AMQPEnvelope::getAppId' => ['string|null'], -'AMQPEnvelope::getBody' => ['string'], -'AMQPEnvelope::getContentEncoding' => ['string|null'], -'AMQPEnvelope::getContentType' => ['string|null'], -'AMQPEnvelope::getCorrelationId' => ['string|null'], -'AMQPEnvelope::getDeliveryMode' => ['int'], -'AMQPEnvelope::getDeliveryTag' => ['int|null'], -'AMQPEnvelope::getExchangeName' => ['string|null'], -'AMQPEnvelope::getExpiration' => ['string|null'], -'AMQPEnvelope::getHeader' => ['mixed', 'headerName'=>'string'], 'AMQPEnvelope::getHeaders' => ['array'], -'AMQPEnvelope::getMessageId' => ['string|null'], 'AMQPEnvelope::getPriority' => ['int<0, max>'], -'AMQPEnvelope::getReplyTo' => ['string|null'], -'AMQPEnvelope::getRoutingKey' => ['string'], -'AMQPEnvelope::getTimestamp' => ['int|null'], -'AMQPEnvelope::getType' => ['string|null'], -'AMQPEnvelope::getUserId' => ['string|null'], -'AMQPEnvelope::isRedelivery' => ['bool'], -'AMQPExchange::__construct' => ['void', 'channel'=>'AMQPChannel'], 'AMQPExchange::bind' => ['void', 'exchangeName'=>'string', 'routingKey='=>'string|null', 'arguments='=>'array'], -'AMQPExchange::declareExchange' => ['void'], -'AMQPExchange::delete' => ['void', 'exchangeName='=>'string', 'flags='=>'int'], -'AMQPExchange::getArgument' => ['scalar|null', 'argumentName'=>'string'], 'AMQPExchange::getArguments' => ['array'], -'AMQPExchange::getChannel' => ['AMQPChannel'], -'AMQPExchange::getConnection' => ['AMQPConnection'], -'AMQPExchange::getFlags' => ['int'], -'AMQPExchange::getName' => ['string|null'], -'AMQPExchange::getType' => ['string|null'], 'AMQPExchange::publish' => ['void', 'message'=>'string', 'routingKey='=>'string|null', 'flags='=>'int|null', 'header='=>'array'], 'AMQPExchange::setArgument' => ['void', 'argumentName'=>'string', 'argumentValue'=>'scalar|null'], 'AMQPExchange::setArguments' => ['void', 'arguments'=>'array'], -'AMQPExchange::setFlags' => ['void', 'flags'=>'int|null'], 'AMQPExchange::setName' => ['void', 'exchangeName'=>'string|null'], 'AMQPExchange::setType' => ['void', 'exchangeType'=>'string|null'], 'AMQPExchange::unbind' => ['void', 'exchangeName'=>'string', 'routingKey='=>'string|null', 'arguments='=>'array'], -'AMQPQueue::__construct' => ['void', 'channel'=>'AMQPChannel'], -'AMQPQueue::ack' => ['void', 'deliveryTag'=>'int', 'flags='=>'int|null'], 'AMQPQueue::bind' => ['void', 'exchangeName'=>'string', 'routingKey='=>'string|null', 'arguments='=>'array'], 'AMQPQueue::cancel' => ['void', 'consumerTag='=>'string'], 'AMQPQueue::consume' => ['void', 'callback='=>'null|callable(AMQPEnvelope, AMQPQueue): mixed', 'flags='=>'int|null', 'consumerTag='=>'string|null'], -'AMQPQueue::declareQueue' => ['int'], 'AMQPQueue::delete' => ['int', 'flags='=>'int|null'], -'AMQPQueue::get' => ['AMQPEnvelope|null', 'flags='=>'int|null'], -'AMQPQueue::getArgument' => ['scalar|null|array|AMQPValue|AMQPDecimal|AMQPTimestamp', 'argumentName'=>'string'], -'AMQPQueue::getArguments' => ['array'], -'AMQPQueue::getChannel' => ['AMQPChannel'], -'AMQPQueue::getConnection' => ['AMQPConnection'], -'AMQPQueue::getFlags' => ['int'], -'AMQPQueue::getName' => ['string|null'], 'AMQPQueue::nack' => ['void', 'deliveryTag'=>'int', 'flags='=>'int|null'], -'AMQPQueue::purge' => ['int'], 'AMQPQueue::reject' => ['void', 'deliveryTag'=>'int', 'flags='=>'int|null'], -'AMQPQueue::setArgument' => ['void', 'argumentName'=>'string', 'argumentValue'=>'scalar|null|array|AMQPValue|AMQPDecimal|AMQPTimestamp'], -'AMQPQueue::setArguments' => ['void', 'arguments'=>'array'], 'AMQPQueue::setFlags' => ['void', 'flags'=>'int|null'], 'AMQPQueue::setName' => ['void', 'name'=>'string'], 'AMQPQueue::unbind' => ['void', 'exchangeName'=>'string', 'routingKey='=>'string|null', 'arguments='=>'array'], @@ -211,7 +135,7 @@ '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'], @@ -222,7 +146,7 @@ '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'], @@ -288,7 +212,7 @@ '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'], @@ -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'], @@ -996,8 +920,8 @@ 'closelog' => ['bool'], 'Closure::__construct' => ['void'], 'Closure::__invoke' => ['', '...args='=>''], -'Closure::bind' => ['Closure', 'old'=>'Closure', 'to'=>'?object', 'scope='=>'object|string|null'], -'Closure::bindTo' => ['Closure', 'new'=>'?object', 'newscope='=>'object|string|null'], +'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'], @@ -1070,302 +994,7 @@ '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' => ['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'], @@ -1494,12 +1123,12 @@ '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'], 'curl_multi_remove_handle' => ['int', 'mh'=>'resource', 'ch'=>'resource'], @@ -1900,15 +1529,15 @@ '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'], @@ -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::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'], @@ -2022,277 +1651,107 @@ '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'=>'scalar|array|resource|null'], -'Ds\Collection::clear' => ['void'], -'Ds\Collection::copy' => ['Ds\Collection'], -'Ds\Collection::isEmpty' => ['bool'], -'Ds\Collection::toArray' => ['array'], '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::map' => ['Ds\Set', 'callback='=>'callable'], -'Ds\Set::merge' => ['Ds\Set', 'values'=>'mixed'], -'Ds\Set::reduce' => ['mixed', 'callback'=>'callable', 'initial='=>'mixed'], 'Ds\Set::remove' => ['void', '...values='=>'mixed'], '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'], -'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'], +'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'], @@ -2336,7 +1795,7 @@ '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)'], @@ -2639,7 +2098,7 @@ 'explode' => ['list|false', 'separator'=>'string', 'str'=>'string', 'limit='=>'int'], 'expm1' => ['float', 'number'=>'float'], 'extension_loaded' => ['bool', 'extension_name'=>'string'], -'extract' => ['0|positive-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'], @@ -2934,10 +2393,10 @@ 'ffmpeg_movie::hasAudio' => ['bool'], 'ffmpeg_movie::hasVideo' => ['bool'], 'fgetc' => ['string|false', 'fp'=>'resource'], -'fgetcsv' => ['list|array{0: null}|false|null', 'fp'=>'resource', 'length='=>'0|positive-int', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], +'fgetcsv' => ['list|array{0: null}|false|null', 'fp'=>'resource', 'length='=>'0|positive-int|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'fgets' => ['string|false', 'fp'=>'resource', 'length='=>'0|positive-int'], 'fgetss' => ['string|false', 'fp'=>'resource', 'length='=>'0|positive-int', 'allowable_tags='=>'string'], -'file' => ['list|false', 'filename'=>'string', 'flags='=>'int', 'context='=>'resource'], +'file' => ['list|false', 'filename'=>'string', 'flags='=>'int-mask', 'context='=>'resource'], 'file_exists' => ['bool', 'filename'=>'string'], 'file_get_contents' => ['string|false', 'filename'=>'string', 'use_include_path='=>'bool', 'context='=>'?resource', 'offset='=>'int', 'maxlen='=>'0|positive-int'], 'file_put_contents' => ['0|positive-int|false', 'file'=>'string', 'data'=>'mixed', 'flags='=>'int', 'context='=>'?resource'], @@ -2990,7 +2449,7 @@ 'finfo_open' => ['resource|false', 'options='=>'int', 'arg='=>'string'], 'finfo_set_flags' => ['bool', 'finfo'=>'resource', 'options'=>'int'], 'floatval' => ['float', 'var'=>'scalar|array|resource|null'], -'flock' => ['bool', 'fp'=>'resource', 'operation'=>'int', '&w_wouldblock='=>'int'], +'flock' => ['bool', 'fp'=>'resource', 'operation'=>'int-mask', '&w_wouldblock='=>'0|1'], 'floor' => ['__benevolent', 'number'=>'float'], 'flush' => ['void'], 'fmod' => ['float', 'x'=>'float', 'y'=>'float'], @@ -3013,7 +2472,7 @@ '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'], @@ -3021,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|false', 'stream'=>'resource', 'local_file'=>'string', 'remote_file'=>'string', 'mode='=>'int', 'resume_pos='=>'int'], -'ftp_nb_put' => ['int|false', 'stream'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode='=>'int', 'startpos='=>'int'], +'ftp_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'], @@ -3305,9 +2764,9 @@ 'get_declared_classes' => ['list'], 'get_declared_interfaces' => ['list'], 'get_declared_traits' => ['list'], -'get_defined_constants' => ['array', 'categorize='=>'bool'], +'get_defined_constants' => ['array', 'categorize='=>'bool'], 'get_defined_functions' => ['array{internal:non-empty-list,user:list}', 'exclude_disabled='=>'bool'], -'get_defined_vars' => ['array'], +'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'], @@ -3911,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' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], +'hash' => ['non-falsy-string|false', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], 'hash_algos' => ['non-empty-list'], 'hash_copy' => ['HashContext', 'context'=>'HashContext'], 'hash_equals' => ['bool', 'known_string'=>'string', 'user_string'=>'string'], -'hash_file' => ['non-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'raw_output='=>'bool'], -'hash_final' => ['non-empty-string', 'context'=>'HashContext', 'raw_output='=>'bool'], -'hash_hkdf' => ['non-empty-string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], -'hash_hmac' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], +'hash_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-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'key'=>'string', 'raw_output='=>'bool'], +'hash_hmac_file' => ['non-falsy-string|false', 'algo'=>'string', 'filename'=>'string', 'key'=>'string', 'raw_output='=>'bool'], 'hash_init' => ['HashContext', 'algo'=>'string', 'options='=>'int', 'key='=>'string'], -'hash_pbkdf2' => ['non-empty-string|false', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], +'hash_pbkdf2' => ['(non-falsy-string&lowercase-string)|false', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], 'hash_update' => ['bool', 'context'=>'HashContext', 'data'=>'string'], 'hash_update_file' => ['bool', 'context'=>'HashContext', 'filename'=>'string', 'scontext='=>'?HashContext'], 'hash_update_stream' => ['int', 'context'=>'HashContext', 'handle'=>'resource', 'length='=>'int'], @@ -3959,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'], @@ -4544,19 +4003,19 @@ '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<0, max>|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorallocatealpha' => ['int<0, max>|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'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', 'green'=>'int', 'blue'=>'int'], -'imagecolorclosestalpha' => ['int<0, max>', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], -'imagecolorclosesthwb' => ['int<0, max>', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'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<0, max>|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorexactalpha' => ['int<0, max>|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<0, max>', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorresolvealpha' => ['int<0, max>', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], -'imagecolorset' => ['void', 'im'=>'resource', 'col'=>'int', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha='=>'int'], +'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'], @@ -4566,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' => ['__benevolent', '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'], @@ -4579,7 +4038,7 @@ 'imagecreatefromwebp' => ['resource|false', 'filename'=>'string'], 'imagecreatefromxbm' => ['resource|false', 'filename'=>'string'], 'imagecreatefromxpm' => ['resource|false', 'filename'=>'string'], -'imagecreatetruecolor' => ['__benevolent', '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'], @@ -4650,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='=>'int'], +'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'], @@ -4682,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{Imagick,float}', 'image'=>'imagick', 'channeltype'=>'int', 'metrictype'=>'int'], -'Imagick::compareImageLayers' => ['Imagick', 'method'=>'int'], -'Imagick::compareImages' => ['array{Imagick,float}', 'compare'=>'imagick', 'metric'=>'int'], -'Imagick::compositeImage' => ['bool', 'composite_object'=>'imagick', 'composite'=>'int', 'x'=>'int', 'y'=>'int', 'channel='=>'int'], +'Imagick::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'], @@ -4706,32 +4165,32 @@ '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' => ['list', '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::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'], @@ -4748,13 +4207,13 @@ 'Imagick::getImageBlob' => ['string'], '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{minima:0|positive-int,maxima:0|positive-int}', 'channel'=>'int'], -'Imagick::getImageChannelKurtosis' => ['array{kurtosis:float,skewness:float}', 'channel='=>'int'], -'Imagick::getImageChannelMean' => ['array{mean:float,standardDeviation:float}', 'channel'=>'int'], -'Imagick::getImageChannelRange' => ['array{minima:float,maxima:float}', 'channel'=>'int'], +'Imagick::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'], @@ -4766,7 +4225,7 @@ 'Imagick::getImageDelay' => ['int'], 'Imagick::getImageDepth' => ['int'], 'Imagick::getImageDispose' => ['Imagick::DISPOSE_*'], -'Imagick::getImageDistortion' => ['float', 'reference'=>'magickwand', 'metric'=>'int'], +'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'], @@ -4821,8 +4280,8 @@ '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::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'], @@ -4834,11 +4293,11 @@ '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'], @@ -4847,26 +4306,26 @@ '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'], @@ -4881,16 +4340,16 @@ '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='=>'int'], +'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'], @@ -4901,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' => ['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', 'legacy='=>'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::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'], @@ -4930,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'], @@ -4981,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'=>'0|positive-int'], -'Imagick::sparseColorImage' => ['bool', 'sparse_method'=>'int', 'arguments'=>'array', 'channel='=>'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'], @@ -5029,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'], @@ -5071,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'], @@ -5112,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'], @@ -5135,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'], @@ -5157,10 +4616,10 @@ 'ImagickDraw::translate' => ['bool', 'x'=>'float', 'y'=>'float'], 'ImagickKernel::addKernel' => ['void', 'ImagickKernel'=>'ImagickKernel'], 'ImagickKernel::addUnityKernel' => ['void'], -'ImagickKernel::fromBuiltin' => ['ImagickKernel', 'kernelType'=>'int', 'kernelString'=>'string'], +'ImagickKernel::fromBuiltin' => ['ImagickKernel', 'kernelType'=>'Imagick::KERNEL_*', 'kernelString'=>'string'], 'ImagickKernel::fromMatrix' => ['ImagickKernel', 'matrix'=>'array', 'origin='=>'array'], 'ImagickKernel::getMatrix' => ['list>'], -'ImagickKernel::scale' => ['void', 'scale'=>'float', 'normalizeFlag'=>'int'], +'ImagickKernel::scale' => ['void', 'scale'=>'float', 'normalizeFlag'=>'Imagick::NORMALIZE_KERNEL_*'], 'ImagickKernel::separate' => ['array'], 'ImagickPixel::__construct' => ['void', 'color='=>'string'], 'ImagickPixel::clear' => ['bool'], @@ -5629,7 +5088,7 @@ 'InvalidArgumentException::getTraceAsString' => ['string'], 'ip2long' => ['int|false', 'ip_address'=>'string'], 'iptcembed' => ['string|bool', 'iptcdata'=>'string', 'jpeg_file_name'=>'string', 'spool='=>'int'], -'iptcparse' => ['array>|false', 'iptcdata'=>'string'], +'iptcparse' => ['array>|false', 'iptcdata'=>'string'], 'is_a' => ['bool', 'object_or_string'=>'object|string', 'class_name'=>'string', 'allow_string='=>'bool'], 'is_array' => ['bool', 'var'=>'mixed'], 'is_bool' => ['bool', 'var'=>'mixed'], @@ -5928,7 +5387,7 @@ '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'], @@ -5955,15 +5414,15 @@ 'litespeed_response_headers' => ['array|false'], 'Locale::acceptFromHttp' => ['non-empty-string|false', 'header'=>'string'], 'Locale::canonicalize' => ['non-empty-string|null', 'locale'=>'string'], -'Locale::composeLocale' => ['string|false', 'subtags'=>'array'], +'Locale::composeLocale' => ['string|false', 'subtags'=>'array{language:string, script?:string, region?:string, variant?:array, private?:array, extlang?:array, variant0?:string, variant1?:string, variant2?:string, variant3?:string, variant4?:string, variant5?:string, variant6?:string, variant7?:string, variant8?:string, variant9?:string, variant10?:string, variant11?:string, variant12?:string, variant13?:string, variant14?:string, private0?:string, private1?:string, private2?:string, private3?:string, private4?:string, private5?:string, private6?:string, private7?:string, private8?:string, private9?:string, private10?:string, private11?:string, private12?:string, private13?:string, private14?:string, extlang0?:string, extlang1?:string, extlang2?:string}'], 'Locale::filterMatches' => ['bool|null', 'langtag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], 'Locale::getAllVariants' => ['array|null', 'locale'=>'string'], 'Locale::getDefault' => ['non-empty-string'], -'Locale::getDisplayLanguage' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], -'Locale::getDisplayName' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], -'Locale::getDisplayRegion' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], -'Locale::getDisplayScript' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], -'Locale::getDisplayVariant' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], +'Locale::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'], @@ -5973,15 +5432,15 @@ 'Locale::setDefault' => ['bool', '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'], +'locale_compose' => ['string|false', 'subtags'=>'array{language:string, script?:string, region?:string, variant?:array, private?:array, extlang?:array, variant0?:string, variant1?:string, variant2?:string, variant3?:string, variant4?:string, variant5?:string, variant6?:string, variant7?:string, variant8?:string, variant9?:string, variant10?:string, variant11?:string, variant12?:string, variant13?:string, variant14?:string, private0?:string, private1?:string, private2?:string, private3?:string, private4?:string, private5?:string, private6?:string, private7?:string, private8?:string, private9?:string, private10?:string, private11?:string, private12?:string, private13?:string, private14?:string, extlang0?:string, extlang1?:string, extlang2?:string}'], 'locale_filter_matches' => ['bool|null', 'langtag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], 'locale_get_all_variants' => ['array|null', 'locale'=>'string'], 'locale_get_default' => ['non-empty-string'], -'locale_get_display_language' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], -'locale_get_display_name' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], -'locale_get_display_region' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], -'locale_get_display_script' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], -'locale_get_display_variant' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], +'locale_get_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'], @@ -6144,7 +5603,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'=>''], @@ -6314,13 +5773,14 @@ '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|list', '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' => ['list|false', 'encoding'=>'string'], @@ -6363,8 +5823,8 @@ 'mb_strripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], 'mb_strrpos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], 'mb_strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'part='=>'bool', 'encoding='=>'string'], -'mb_strtolower' => ['string', 'str'=>'string', 'encoding='=>'string'], -'mb_strtoupper' => ['string', 'str'=>'string', 'encoding='=>'string'], +'mb_strtolower' => ['lowercase-string', 'str'=>'string', 'encoding='=>'string'], +'mb_strtoupper' => ['uppercase-string', 'str'=>'string', 'encoding='=>'string'], 'mb_strwidth' => ['0|positive-int', 'str'=>'string', 'encoding='=>'string'], 'mb_substitute_character' => ['mixed', 'substchar='=>'mixed'], 'mb_substr' => ['string', 'str'=>'string', 'start'=>'int', 'length='=>'?int', 'encoding='=>'string'], @@ -6405,8 +5865,8 @@ 'mcrypt_module_open' => ['resource|false', 'cipher'=>'string', 'cipher_directory'=>'string', 'mode'=>'string', 'mode_directory'=>'string'], 'mcrypt_module_self_test' => ['bool', 'algorithm'=>'string', 'lib_dir='=>'string'], 'mcrypt_ofb' => ['string', 'cipher'=>'string', 'key'=>'string', 'data'=>'string', 'mode'=>'int', 'iv='=>'string'], -'md5' => ['non-falsy-string', 'str'=>'string', 'raw_output='=>'bool'], -'md5_file' => ['non-falsy-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'], @@ -6516,7 +5976,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'], @@ -6595,7 +6055,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'], @@ -6606,7 +6066,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'], @@ -6714,361 +6174,6 @@ 'MongoDB::setReadPreference' => ['bool', 'read_preference'=>'string', 'tags='=>'array'], 'MongoDB::setSlaveOkay' => ['bool', 'ok='=>'bool'], 'MongoDB::setWriteConcern' => ['bool', 'w'=>'mixed', 'wtimeout='=>'int'], -'MongoDB\BSON\fromJSON' => ['string', 'json'=>'string'], -'MongoDB\BSON\fromPHP' => ['string', 'value'=>'object|array'], -'MongoDB\BSON\toCanonicalExtendedJSON' => ['string', 'bson'=>'string'], -'MongoDB\BSON\toJSON' => ['string', 'bson'=>'string'], -'MongoDB\BSON\toPHP' => ['object|array', 'bson'=>'string', 'typemap='=>'?array'], -'MongoDB\BSON\toRelaxedExtendedJSON' => ['string', 'bson'=>'string'], -'MongoDB\Driver\Monitoring\addSubscriber' => ['void', 'subscriber'=>'MongoDB\Driver\Monitoring\Subscriber'], -'MongoDB\Driver\Monitoring\removeSubscriber' => ['void', 'subscriber'=>'MongoDB\Driver\Monitoring\Subscriber'], -'MongoDB\BSON\Binary::__construct' => ['void', 'data'=>'string', 'type='=>'int'], -'MongoDB\BSON\Binary::getData' => ['string'], -'MongoDB\BSON\Binary::getType' => ['int'], -'MongoDB\BSON\Binary::__toString' => ['string'], -'MongoDB\BSON\Binary::serialize' => ['string'], -'MongoDB\BSON\Binary::unserialize' => ['void', 'data'=>'string'], -'MongoDB\BSON\Binary::jsonSerialize' => ['mixed'], -'MongoDB\BSON\BinaryInterface::getData' => ['string'], -'MongoDB\BSON\BinaryInterface::getType' => ['int'], -'MongoDB\BSON\BinaryInterface::__toString' => ['string'], -'MongoDB\BSON\DBPointer::__toString' => ['string'], -'MongoDB\BSON\DBPointer::serialize' => ['string'], -'MongoDB\BSON\DBPointer::unserialize' => ['void', 'data'=>'string'], -'MongoDB\BSON\DBPointer::jsonSerialize' => ['mixed'], -'MongoDB\BSON\Decimal128::__construct' => ['void', 'value'=>'string'], -'MongoDB\BSON\Decimal128::__toString' => ['string'], -'MongoDB\BSON\Decimal128::serialize' => ['string'], -'MongoDB\BSON\Decimal128::unserialize' => ['void', 'data'=>'string'], -'MongoDB\BSON\Decimal128::jsonSerialize' => ['mixed'], -'MongoDB\BSON\Decimal128Interface::__toString' => ['string'], -'MongoDB\BSON\Document::fromBSON' => ['MongoDB\BSON\Document', 'bson'=>'string'], -'MongoDB\BSON\Document::fromJSON' => ['MongoDB\BSON\Document', 'json'=>'string'], -'MongoDB\BSON\Document::fromPHP' => ['MongoDB\BSON\Document', 'value'=>'object|array'], -'MongoDB\BSON\Document::get' => ['mixed', 'key'=>'string'], -'MongoDB\BSON\Document::getIterator' => ['MongoDB\BSON\Iterator'], -'MongoDB\BSON\Document::has' => ['bool', 'key'=>'string'], -'MongoDB\BSON\Document::toPHP' => ['object|array', 'typeMap='=>'?array'], -'MongoDB\BSON\Document::toCanonicalExtendedJSON' => ['string'], -'MongoDB\BSON\Document::toRelaxedExtendedJSON' => ['string'], -'MongoDB\BSON\Document::offsetExists' => ['bool', 'offset'=>'mixed'], -'MongoDB\BSON\Document::offsetGet' => ['mixed', 'offset'=>'mixed'], -'MongoDB\BSON\Document::offsetSet' => ['void', 'offset'=>'mixed', 'value'=>'mixed'], -'MongoDB\BSON\Document::offsetUnset' => ['void', 'offset'=>'mixed'], -'MongoDB\BSON\Document::__toString' => ['string'], -'MongoDB\BSON\Document::serialize' => ['string'], -'MongoDB\BSON\Document::unserialize' => ['void', 'data'=>'string'], -'MongoDB\BSON\Int64::__construct' => ['void', 'value'=>'string|int'], -'MongoDB\BSON\Int64::__toString' => ['string'], -'MongoDB\BSON\Int64::serialize' => ['string'], -'MongoDB\BSON\Int64::unserialize' => ['void', 'data'=>'string'], -'MongoDB\BSON\Int64::jsonSerialize' => ['mixed'], -'MongoDB\BSON\Iterator::current' => ['mixed'], -'MongoDB\BSON\Iterator::key' => ['string|int'], -'MongoDB\BSON\Iterator::next' => ['void'], -'MongoDB\BSON\Iterator::rewind' => ['void'], -'MongoDB\BSON\Iterator::valid' => ['bool'], -'MongoDB\BSON\Javascript::__construct' => ['void', 'code'=>'string', 'scope='=>'object|array|null'], -'MongoDB\BSON\Javascript::getCode' => ['string'], -'MongoDB\BSON\Javascript::getScope' => ['?object'], -'MongoDB\BSON\Javascript::__toString' => ['string'], -'MongoDB\BSON\Javascript::serialize' => ['string'], -'MongoDB\BSON\Javascript::unserialize' => ['void', 'data'=>'string'], -'MongoDB\BSON\Javascript::jsonSerialize' => ['mixed'], -'MongoDB\BSON\JavascriptInterface::getCode' => ['string'], -'MongoDB\BSON\JavascriptInterface::getScope' => ['?object'], -'MongoDB\BSON\JavascriptInterface::__toString' => ['string'], -'MongoDB\BSON\MaxKey::serialize' => ['string'], -'MongoDB\BSON\MaxKey::unserialize' => ['void', 'data'=>'string'], -'MongoDB\BSON\MaxKey::jsonSerialize' => ['mixed'], -'MongoDB\BSON\MinKey::serialize' => ['string'], -'MongoDB\BSON\MinKey::unserialize' => ['void', 'data'=>'string'], -'MongoDB\BSON\MinKey::jsonSerialize' => ['mixed'], -'MongoDB\BSON\ObjectId::__construct' => ['void', 'id='=>'?string'], -'MongoDB\BSON\ObjectId::getTimestamp' => ['int'], -'MongoDB\BSON\ObjectId::__toString' => ['string'], -'MongoDB\BSON\ObjectId::serialize' => ['string'], -'MongoDB\BSON\ObjectId::unserialize' => ['void', 'data'=>'string'], -'MongoDB\BSON\ObjectId::jsonSerialize' => ['mixed'], -'MongoDB\BSON\ObjectIdInterface::getTimestamp' => ['int'], -'MongoDB\BSON\ObjectIdInterface::__toString' => ['string'], -'MongoDB\BSON\PackedArray::fromPHP' => ['MongoDB\BSON\PackedArray', 'value'=>'array'], -'MongoDB\BSON\PackedArray::get' => ['mixed', 'index'=>'int'], -'MongoDB\BSON\PackedArray::getIterator' => ['MongoDB\BSON\Iterator'], -'MongoDB\BSON\PackedArray::has' => ['bool', 'index'=>'int'], -'MongoDB\BSON\PackedArray::toPHP' => ['object|array', 'typeMap='=>'?array'], -'MongoDB\BSON\PackedArray::offsetExists' => ['bool', 'offset'=>'mixed'], -'MongoDB\BSON\PackedArray::offsetGet' => ['mixed', 'offset'=>'mixed'], -'MongoDB\BSON\PackedArray::offsetSet' => ['void', 'offset'=>'mixed', 'value'=>'mixed'], -'MongoDB\BSON\PackedArray::offsetUnset' => ['void', 'offset'=>'mixed'], -'MongoDB\BSON\PackedArray::__toString' => ['string'], -'MongoDB\BSON\PackedArray::serialize' => ['string'], -'MongoDB\BSON\PackedArray::unserialize' => ['void', 'data'=>'string'], -'MongoDB\BSON\Persistable::bsonSerialize' => ['stdClass|MongoDB\BSON\Document|array'], -'MongoDB\BSON\Regex::__construct' => ['void', 'pattern'=>'string', 'flags='=>'string'], -'MongoDB\BSON\Regex::getPattern' => ['string'], -'MongoDB\BSON\Regex::getFlags' => ['string'], -'MongoDB\BSON\Regex::__toString' => ['string'], -'MongoDB\BSON\Regex::serialize' => ['string'], -'MongoDB\BSON\Regex::unserialize' => ['void', 'data'=>'string'], -'MongoDB\BSON\Regex::jsonSerialize' => ['mixed'], -'MongoDB\BSON\RegexInterface::getPattern' => ['string'], -'MongoDB\BSON\RegexInterface::getFlags' => ['string'], -'MongoDB\BSON\RegexInterface::__toString' => ['string'], -'MongoDB\BSON\Serializable::bsonSerialize' => ['stdClass|MongoDB\BSON\Document|MongoDB\BSON\PackedArray|array'], -'MongoDB\BSON\Symbol::__toString' => ['string'], -'MongoDB\BSON\Symbol::serialize' => ['string'], -'MongoDB\BSON\Symbol::unserialize' => ['void', 'data'=>'string'], -'MongoDB\BSON\Symbol::jsonSerialize' => ['mixed'], -'MongoDB\BSON\Timestamp::__construct' => ['void', 'increment'=>'string|int', 'timestamp'=>'string|int'], -'MongoDB\BSON\Timestamp::getTimestamp' => ['int'], -'MongoDB\BSON\Timestamp::getIncrement' => ['int'], -'MongoDB\BSON\Timestamp::__toString' => ['string'], -'MongoDB\BSON\Timestamp::serialize' => ['string'], -'MongoDB\BSON\Timestamp::unserialize' => ['void', 'data'=>'string'], -'MongoDB\BSON\Timestamp::jsonSerialize' => ['mixed'], -'MongoDB\BSON\TimestampInterface::getTimestamp' => ['int'], -'MongoDB\BSON\TimestampInterface::getIncrement' => ['int'], -'MongoDB\BSON\TimestampInterface::__toString' => ['string'], -'MongoDB\BSON\UTCDateTime::__construct' => ['void', 'milliseconds='=>'DateTimeInterface|string|int|float|null'], -'MongoDB\BSON\UTCDateTime::toDateTime' => ['DateTime'], -'MongoDB\BSON\UTCDateTime::__toString' => ['string'], -'MongoDB\BSON\UTCDateTime::serialize' => ['string'], -'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'data'=>'string'], -'MongoDB\BSON\UTCDateTime::jsonSerialize' => ['mixed'], -'MongoDB\BSON\UTCDateTimeInterface::toDateTime' => ['DateTime'], -'MongoDB\BSON\UTCDateTimeInterface::__toString' => ['string'], -'MongoDB\BSON\Undefined::__toString' => ['string'], -'MongoDB\BSON\Undefined::serialize' => ['string'], -'MongoDB\BSON\Undefined::unserialize' => ['void', 'data'=>'string'], -'MongoDB\BSON\Undefined::jsonSerialize' => ['mixed'], -'MongoDB\BSON\Unserializable::bsonUnserialize' => ['void', 'data'=>'array'], -'MongoDB\Driver\BulkWrite::__construct' => ['void', 'options='=>'?array'], -'MongoDB\Driver\BulkWrite::count' => ['int'], -'MongoDB\Driver\BulkWrite::delete' => ['void', 'filter'=>'object|array', 'deleteOptions='=>'?array'], -'MongoDB\Driver\BulkWrite::insert' => ['mixed', 'document'=>'object|array'], -'MongoDB\Driver\BulkWrite::update' => ['void', 'filter'=>'object|array', 'newObj'=>'object|array', 'updateOptions='=>'?array'], -'MongoDB\Driver\ClientEncryption::__construct' => ['void', 'options'=>'array'], -'MongoDB\Driver\ClientEncryption::addKeyAltName' => ['?object', 'keyId'=>'MongoDB\BSON\Binary', 'keyAltName'=>'string'], -'MongoDB\Driver\ClientEncryption::createDataKey' => ['MongoDB\BSON\Binary', 'kmsProvider'=>'string', 'options='=>'?array'], -'MongoDB\Driver\ClientEncryption::decrypt' => ['mixed', 'value'=>'MongoDB\BSON\Binary'], -'MongoDB\Driver\ClientEncryption::deleteKey' => ['object', 'keyId'=>'MongoDB\BSON\Binary'], -'MongoDB\Driver\ClientEncryption::encrypt' => ['MongoDB\BSON\Binary', 'value'=>'mixed', 'options='=>'?array'], -'MongoDB\Driver\ClientEncryption::encryptExpression' => ['object', 'expr'=>'object|array', 'options='=>'?array'], -'MongoDB\Driver\ClientEncryption::getKey' => ['?object', 'keyId'=>'MongoDB\BSON\Binary'], -'MongoDB\Driver\ClientEncryption::getKeyByAltName' => ['?object', 'keyAltName'=>'string'], -'MongoDB\Driver\ClientEncryption::getKeys' => ['MongoDB\Driver\Cursor'], -'MongoDB\Driver\ClientEncryption::removeKeyAltName' => ['?object', 'keyId'=>'MongoDB\BSON\Binary', 'keyAltName'=>'string'], -'MongoDB\Driver\ClientEncryption::rewrapManyDataKey' => ['object', 'filter'=>'object|array', 'options='=>'?array'], -'MongoDB\Driver\Command::__construct' => ['void', 'document'=>'object|array', 'commandOptions='=>'?array'], -'MongoDB\Driver\Cursor::current' => ['object|array|null'], -'MongoDB\Driver\Cursor::getId' => ['MongoDB\Driver\CursorId'], -'MongoDB\Driver\Cursor::getServer' => ['MongoDB\Driver\Server'], -'MongoDB\Driver\Cursor::isDead' => ['bool'], -'MongoDB\Driver\Cursor::key' => ['?int'], -'MongoDB\Driver\Cursor::next' => ['void'], -'MongoDB\Driver\Cursor::rewind' => ['void'], -'MongoDB\Driver\Cursor::setTypeMap' => ['void', 'typemap'=>'array'], -'MongoDB\Driver\Cursor::toArray' => ['array'], -'MongoDB\Driver\Cursor::valid' => ['bool'], -'MongoDB\Driver\CursorId::__toString' => ['string'], -'MongoDB\Driver\CursorId::serialize' => ['string'], -'MongoDB\Driver\CursorId::unserialize' => ['void', 'data'=>'string'], -'MongoDB\Driver\CursorInterface::getId' => ['MongoDB\Driver\CursorId'], -'MongoDB\Driver\CursorInterface::getServer' => ['MongoDB\Driver\Server'], -'MongoDB\Driver\CursorInterface::isDead' => ['bool'], -'MongoDB\Driver\CursorInterface::setTypeMap' => ['void', 'typemap'=>'array'], -'MongoDB\Driver\CursorInterface::toArray' => ['array'], -'MongoDB\Driver\Exception\AuthenticationException::__toString' => ['string'], -'MongoDB\Driver\Exception\BulkWriteException::__toString' => ['string'], -'MongoDB\Driver\Exception\CommandException::getResultDocument' => ['object'], -'MongoDB\Driver\Exception\CommandException::__toString' => ['string'], -'MongoDB\Driver\Exception\ConnectionException::__toString' => ['string'], -'MongoDB\Driver\Exception\ConnectionTimeoutException::__toString' => ['string'], -'MongoDB\Driver\Exception\EncryptionException::__toString' => ['string'], -'MongoDB\Driver\Exception\Exception::__toString' => ['string'], -'MongoDB\Driver\Exception\ExecutionTimeoutException::__toString' => ['string'], -'MongoDB\Driver\Exception\InvalidArgumentException::__toString' => ['string'], -'MongoDB\Driver\Exception\LogicException::__toString' => ['string'], -'MongoDB\Driver\Exception\RuntimeException::hasErrorLabel' => ['bool', 'errorLabel'=>'string'], -'MongoDB\Driver\Exception\RuntimeException::__toString' => ['string'], -'MongoDB\Driver\Exception\SSLConnectionException::__toString' => ['string'], -'MongoDB\Driver\Exception\ServerException::__toString' => ['string'], -'MongoDB\Driver\Exception\UnexpectedValueException::__toString' => ['string'], -'MongoDB\Driver\Exception\WriteException::getWriteResult' => ['MongoDB\Driver\WriteResult'], -'MongoDB\Driver\Exception\WriteException::__toString' => ['string'], -'MongoDB\Driver\Manager::__construct' => ['void', 'uri='=>'?string', 'uriOptions='=>'?array', 'driverOptions='=>'?array'], -'MongoDB\Driver\Manager::addSubscriber' => ['void', 'subscriber'=>'MongoDB\Driver\Monitoring\Subscriber'], -'MongoDB\Driver\Manager::createClientEncryption' => ['MongoDB\Driver\ClientEncryption', 'options'=>'array'], -'MongoDB\Driver\Manager::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'bulk'=>'MongoDB\Driver\BulkWrite', 'options='=>'MongoDB\Driver\WriteConcern|array|null'], -'MongoDB\Driver\Manager::executeCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'MongoDB\Driver\ReadPreference|array|null'], -'MongoDB\Driver\Manager::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace'=>'string', 'query'=>'MongoDB\Driver\Query', 'options='=>'MongoDB\Driver\ReadPreference|array|null'], -'MongoDB\Driver\Manager::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], -'MongoDB\Driver\Manager::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], -'MongoDB\Driver\Manager::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], -'MongoDB\Driver\Manager::getEncryptedFieldsMap' => ['object|array|null'], -'MongoDB\Driver\Manager::getReadConcern' => ['MongoDB\Driver\ReadConcern'], -'MongoDB\Driver\Manager::getReadPreference' => ['MongoDB\Driver\ReadPreference'], -'MongoDB\Driver\Manager::getServers' => ['array'], -'MongoDB\Driver\Manager::getWriteConcern' => ['MongoDB\Driver\WriteConcern'], -'MongoDB\Driver\Manager::removeSubscriber' => ['void', 'subscriber'=>'MongoDB\Driver\Monitoring\Subscriber'], -'MongoDB\Driver\Manager::selectServer' => ['MongoDB\Driver\Server', 'readPreference='=>'?MongoDB\Driver\ReadPreference'], -'MongoDB\Driver\Manager::startSession' => ['MongoDB\Driver\Session', 'options='=>'?array'], -'MongoDB\Driver\Monitoring\CommandFailedEvent::getCommandName' => ['string'], -'MongoDB\Driver\Monitoring\CommandFailedEvent::getDurationMicros' => ['int'], -'MongoDB\Driver\Monitoring\CommandFailedEvent::getError' => ['Exception'], -'MongoDB\Driver\Monitoring\CommandFailedEvent::getOperationId' => ['string'], -'MongoDB\Driver\Monitoring\CommandFailedEvent::getReply' => ['object'], -'MongoDB\Driver\Monitoring\CommandFailedEvent::getRequestId' => ['string'], -'MongoDB\Driver\Monitoring\CommandFailedEvent::getServer' => ['MongoDB\Driver\Server'], -'MongoDB\Driver\Monitoring\CommandFailedEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], -'MongoDB\Driver\Monitoring\CommandFailedEvent::getServerConnectionId' => ['?int'], -'MongoDB\Driver\Monitoring\CommandStartedEvent::getCommand' => ['object'], -'MongoDB\Driver\Monitoring\CommandStartedEvent::getCommandName' => ['string'], -'MongoDB\Driver\Monitoring\CommandStartedEvent::getDatabaseName' => ['string'], -'MongoDB\Driver\Monitoring\CommandStartedEvent::getOperationId' => ['string'], -'MongoDB\Driver\Monitoring\CommandStartedEvent::getRequestId' => ['string'], -'MongoDB\Driver\Monitoring\CommandStartedEvent::getServer' => ['MongoDB\Driver\Server'], -'MongoDB\Driver\Monitoring\CommandStartedEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], -'MongoDB\Driver\Monitoring\CommandStartedEvent::getServerConnectionId' => ['?int'], -'MongoDB\Driver\Monitoring\CommandSubscriber::commandStarted' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandStartedEvent'], -'MongoDB\Driver\Monitoring\CommandSubscriber::commandSucceeded' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandSucceededEvent'], -'MongoDB\Driver\Monitoring\CommandSubscriber::commandFailed' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandFailedEvent'], -'MongoDB\Driver\Monitoring\CommandSucceededEvent::getCommandName' => ['string'], -'MongoDB\Driver\Monitoring\CommandSucceededEvent::getDurationMicros' => ['int'], -'MongoDB\Driver\Monitoring\CommandSucceededEvent::getOperationId' => ['string'], -'MongoDB\Driver\Monitoring\CommandSucceededEvent::getReply' => ['object'], -'MongoDB\Driver\Monitoring\CommandSucceededEvent::getRequestId' => ['string'], -'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServer' => ['MongoDB\Driver\Server'], -'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], -'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServerConnectionId' => ['?int'], -'MongoDB\Driver\Monitoring\LogSubscriber::log' => ['void', 'level'=>'int', 'domain'=>'string', 'message'=>'string'], -'MongoDB\Driver\Monitoring\SDAMSubscriber::serverChanged' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerChangedEvent'], -'MongoDB\Driver\Monitoring\SDAMSubscriber::serverClosed' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerClosedEvent'], -'MongoDB\Driver\Monitoring\SDAMSubscriber::serverOpening' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerOpeningEvent'], -'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatFailed' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent'], -'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatStarted' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent'], -'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatSucceeded' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent'], -'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyChanged' => ['void', 'event'=>'MongoDB\Driver\Monitoring\TopologyChangedEvent'], -'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyClosed' => ['void', 'event'=>'MongoDB\Driver\Monitoring\TopologyClosedEvent'], -'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyOpening' => ['void', 'event'=>'MongoDB\Driver\Monitoring\TopologyOpeningEvent'], -'MongoDB\Driver\Monitoring\ServerChangedEvent::getPort' => ['int'], -'MongoDB\Driver\Monitoring\ServerChangedEvent::getHost' => ['string'], -'MongoDB\Driver\Monitoring\ServerChangedEvent::getNewDescription' => ['MongoDB\Driver\ServerDescription'], -'MongoDB\Driver\Monitoring\ServerChangedEvent::getPreviousDescription' => ['MongoDB\Driver\ServerDescription'], -'MongoDB\Driver\Monitoring\ServerChangedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], -'MongoDB\Driver\Monitoring\ServerClosedEvent::getPort' => ['int'], -'MongoDB\Driver\Monitoring\ServerClosedEvent::getHost' => ['string'], -'MongoDB\Driver\Monitoring\ServerClosedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], -'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getDurationMicros' => ['int'], -'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getError' => ['Exception'], -'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getPort' => ['int'], -'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getHost' => ['string'], -'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::isAwaited' => ['bool'], -'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::getPort' => ['int'], -'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::getHost' => ['string'], -'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::isAwaited' => ['bool'], -'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getDurationMicros' => ['int'], -'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getReply' => ['object'], -'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getPort' => ['int'], -'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getHost' => ['string'], -'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::isAwaited' => ['bool'], -'MongoDB\Driver\Monitoring\ServerOpeningEvent::getPort' => ['int'], -'MongoDB\Driver\Monitoring\ServerOpeningEvent::getHost' => ['string'], -'MongoDB\Driver\Monitoring\ServerOpeningEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], -'MongoDB\Driver\Monitoring\TopologyChangedEvent::getNewDescription' => ['MongoDB\Driver\TopologyDescription'], -'MongoDB\Driver\Monitoring\TopologyChangedEvent::getPreviousDescription' => ['MongoDB\Driver\TopologyDescription'], -'MongoDB\Driver\Monitoring\TopologyChangedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], -'MongoDB\Driver\Monitoring\TopologyClosedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], -'MongoDB\Driver\Monitoring\TopologyOpeningEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], -'MongoDB\Driver\Query::__construct' => ['void', 'filter'=>'object|array', 'queryOptions='=>'?array'], -'MongoDB\Driver\ReadConcern::__construct' => ['void', 'level='=>'?string'], -'MongoDB\Driver\ReadConcern::getLevel' => ['?string'], -'MongoDB\Driver\ReadConcern::isDefault' => ['bool'], -'MongoDB\Driver\ReadConcern::bsonSerialize' => ['stdClass'], -'MongoDB\Driver\ReadConcern::serialize' => ['string'], -'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'data'=>'string'], -'MongoDB\Driver\ReadPreference::__construct' => ['void', 'mode'=>'string|int', 'tagSets='=>'?array', 'options='=>'?array'], -'MongoDB\Driver\ReadPreference::getHedge' => ['?object'], -'MongoDB\Driver\ReadPreference::getMaxStalenessSeconds' => ['int'], -'MongoDB\Driver\ReadPreference::getMode' => ['int'], -'MongoDB\Driver\ReadPreference::getModeString' => ['string'], -'MongoDB\Driver\ReadPreference::getTagSets' => ['array'], -'MongoDB\Driver\ReadPreference::bsonSerialize' => ['stdClass'], -'MongoDB\Driver\ReadPreference::serialize' => ['string'], -'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'data'=>'string'], -'MongoDB\Driver\Server::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'bulkWrite'=>'MongoDB\Driver\BulkWrite', 'options='=>'MongoDB\Driver\WriteConcern|array|null'], -'MongoDB\Driver\Server::executeCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'MongoDB\Driver\ReadPreference|array|null'], -'MongoDB\Driver\Server::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace'=>'string', 'query'=>'MongoDB\Driver\Query', 'options='=>'MongoDB\Driver\ReadPreference|array|null'], -'MongoDB\Driver\Server::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], -'MongoDB\Driver\Server::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], -'MongoDB\Driver\Server::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], -'MongoDB\Driver\Server::getHost' => ['string'], -'MongoDB\Driver\Server::getInfo' => ['array'], -'MongoDB\Driver\Server::getLatency' => ['?int'], -'MongoDB\Driver\Server::getPort' => ['int'], -'MongoDB\Driver\Server::getServerDescription' => ['MongoDB\Driver\ServerDescription'], -'MongoDB\Driver\Server::getTags' => ['array'], -'MongoDB\Driver\Server::getType' => ['int'], -'MongoDB\Driver\Server::isArbiter' => ['bool'], -'MongoDB\Driver\Server::isHidden' => ['bool'], -'MongoDB\Driver\Server::isPassive' => ['bool'], -'MongoDB\Driver\Server::isPrimary' => ['bool'], -'MongoDB\Driver\Server::isSecondary' => ['bool'], -'MongoDB\Driver\ServerApi::__construct' => ['void', 'version'=>'string', 'strict='=>'?bool', 'deprecationErrors='=>'?bool'], -'MongoDB\Driver\ServerApi::bsonSerialize' => ['stdClass'], -'MongoDB\Driver\ServerApi::serialize' => ['string'], -'MongoDB\Driver\ServerApi::unserialize' => ['void', 'data'=>'string'], -'MongoDB\Driver\ServerDescription::getHelloResponse' => ['array'], -'MongoDB\Driver\ServerDescription::getHost' => ['string'], -'MongoDB\Driver\ServerDescription::getLastUpdateTime' => ['int'], -'MongoDB\Driver\ServerDescription::getPort' => ['int'], -'MongoDB\Driver\ServerDescription::getRoundTripTime' => ['?int'], -'MongoDB\Driver\ServerDescription::getType' => ['string'], -'MongoDB\Driver\Session::abortTransaction' => ['void'], -'MongoDB\Driver\Session::advanceClusterTime' => ['void', 'clusterTime'=>'object|array'], -'MongoDB\Driver\Session::advanceOperationTime' => ['void', 'operationTime'=>'MongoDB\BSON\TimestampInterface'], -'MongoDB\Driver\Session::commitTransaction' => ['void'], -'MongoDB\Driver\Session::endSession' => ['void'], -'MongoDB\Driver\Session::getClusterTime' => ['?object'], -'MongoDB\Driver\Session::getLogicalSessionId' => ['object'], -'MongoDB\Driver\Session::getOperationTime' => ['?MongoDB\BSON\Timestamp'], -'MongoDB\Driver\Session::getServer' => ['?MongoDB\Driver\Server'], -'MongoDB\Driver\Session::getTransactionOptions' => ['?array'], -'MongoDB\Driver\Session::getTransactionState' => ['string'], -'MongoDB\Driver\Session::isDirty' => ['bool'], -'MongoDB\Driver\Session::isInTransaction' => ['bool'], -'MongoDB\Driver\Session::startTransaction' => ['void', 'options='=>'?array'], -'MongoDB\Driver\TopologyDescription::getServers' => ['array'], -'MongoDB\Driver\TopologyDescription::getType' => ['string'], -'MongoDB\Driver\TopologyDescription::hasReadableServer' => ['bool', 'readPreference='=>'?MongoDB\Driver\ReadPreference'], -'MongoDB\Driver\TopologyDescription::hasWritableServer' => ['bool'], -'MongoDB\Driver\WriteConcern::__construct' => ['void', 'w'=>'string|int', 'wtimeout='=>'?int', 'journal='=>'?bool'], -'MongoDB\Driver\WriteConcern::getJournal' => ['?bool'], -'MongoDB\Driver\WriteConcern::getW' => ['string|int|null'], -'MongoDB\Driver\WriteConcern::getWtimeout' => ['int'], -'MongoDB\Driver\WriteConcern::isDefault' => ['bool'], -'MongoDB\Driver\WriteConcern::bsonSerialize' => ['stdClass'], -'MongoDB\Driver\WriteConcern::serialize' => ['string'], -'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'data'=>'string'], -'MongoDB\Driver\WriteConcernError::getCode' => ['int'], -'MongoDB\Driver\WriteConcernError::getInfo' => ['?object'], -'MongoDB\Driver\WriteConcernError::getMessage' => ['string'], -'MongoDB\Driver\WriteError::getCode' => ['int'], -'MongoDB\Driver\WriteError::getIndex' => ['int'], -'MongoDB\Driver\WriteError::getInfo' => ['?object'], -'MongoDB\Driver\WriteError::getMessage' => ['string'], -'MongoDB\Driver\WriteResult::getInsertedCount' => ['?int'], -'MongoDB\Driver\WriteResult::getMatchedCount' => ['?int'], -'MongoDB\Driver\WriteResult::getModifiedCount' => ['?int'], -'MongoDB\Driver\WriteResult::getDeletedCount' => ['?int'], -'MongoDB\Driver\WriteResult::getUpsertedCount' => ['?int'], -'MongoDB\Driver\WriteResult::getServer' => ['MongoDB\Driver\Server'], -'MongoDB\Driver\WriteResult::getUpsertedIds' => ['array'], -'MongoDB\Driver\WriteResult::getWriteConcernError' => ['?MongoDB\Driver\WriteConcernError'], -'MongoDB\Driver\WriteResult::getWriteErrors' => ['array'], -'MongoDB\Driver\WriteResult::getErrorReplies' => ['array'], -'MongoDB\Driver\WriteResult::isAcknowledged' => ['bool'], 'MongoDBRef::create' => ['array', 'collection'=>'string', 'id'=>'mixed', 'database='=>'string'], 'MongoDBRef::get' => ['array', 'db'=>'mongodb', 'ref'=>'array'], 'MongoDBRef::isRef' => ['bool', 'ref'=>'mixed'], @@ -7175,7 +6280,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'], @@ -7475,7 +6580,7 @@ 'mysqli_errno' => ['int', 'link'=>'mysqli'], 'mysqli_error' => ['string|null', 'link'=>'mysqli'], 'mysqli_error_list' => ['array', 'connection'=>'mysqli'], -'mysqli_fetch_all' => ['array', 'result'=>'mysqli_result', 'resulttype='=>'int'], +'mysqli_fetch_all' => ['list', 'result'=>'mysqli_result', 'resulttype='=>'int'], 'mysqli_fetch_array' => ['array|null|false', 'result'=>'mysqli_result', 'resulttype='=>'int'], 'mysqli_fetch_assoc' => ['array|null|false', 'result'=>'mysqli_result'], 'mysqli_fetch_column' => ['null|int|float|string|false', 'result' => 'mysqli_result', 'column'=>'int'], @@ -7527,7 +6632,7 @@ '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_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'], @@ -8245,13 +7350,13 @@ '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'], @@ -8266,26 +7371,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'], @@ -8680,7 +7785,7 @@ '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'], @@ -8701,7 +7806,7 @@ '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'], @@ -8723,7 +7828,7 @@ 'PDOStatement::closeCursor' => ['bool'], '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'], @@ -8799,8 +7904,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'], @@ -9050,7 +8155,7 @@ 'posix_getpgrp' => ['int'], 'posix_getpid' => ['int'], 'posix_getppid' => ['int'], -'posix_getpwnam' => ['array|false', 'groupname'=>'string'], +'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'], @@ -9083,7 +8188,7 @@ 'preg_replace' => ['string|array|null', 'regex'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], 'preg_replace_callback' => ['string|array|null', 'regex'=>'string|array', 'callback'=>'callable(array):string', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], 'preg_replace_callback_array' => ['string|array|null', 'pattern'=>'array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], -'preg_split' => ['list|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_r' => ['string|true', 'var'=>'mixed', 'return='=>'bool'], 'printf' => ['int', 'format'=>'string', '...values='=>'__stringAndStringable|int|float|null|bool'], @@ -9440,7 +8545,7 @@ 'RecursiveFilterIterator::hasChildren' => ['bool'], 'RecursiveIterator::getChildren' => ['RecursiveIterator'], 'RecursiveIterator::hasChildren' => ['bool'], -'RecursiveIteratorIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator|IteratorAggregate', 'mode='=>'int', 'flags='=>'int'], +'RecursiveIteratorIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator|IteratorAggregate', 'mode='=>'RecursiveIteratorIterator::LEAVES_ONLY|RecursiveIteratorIterator::SELF_FIRST|RecursiveIteratorIterator::CHILD_FIRST', 'flags='=>'0|RecursiveIteratorIterator::CATCH_GET_CHILD'], 'RecursiveIteratorIterator::beginChildren' => ['void'], 'RecursiveIteratorIterator::beginIteration' => ['RecursiveIterator'], 'RecursiveIteratorIterator::callGetChildren' => ['RecursiveIterator'], @@ -9936,10 +9041,10 @@ 'ReflectionClass::getConstructor' => ['ReflectionMethod|null'], 'ReflectionClass::getDefaultProperties' => ['array'], 'ReflectionClass::getDocComment' => ['string|false'], -'ReflectionClass::getEndLine' => ['int|false'], +'ReflectionClass::getEndLine' => ['positive-int|false'], 'ReflectionClass::getExtension' => ['ReflectionExtension|null'], 'ReflectionClass::getExtensionName' => ['string|false'], -'ReflectionClass::getFileName' => ['string|false'], +'ReflectionClass::getFileName' => ['non-empty-string|false'], 'ReflectionClass::getInterfaceNames' => ['list'], 'ReflectionClass::getInterfaces' => ['array'], 'ReflectionClass::getMethod' => ['ReflectionMethod', 'name'=>'string'], @@ -9953,7 +9058,7 @@ 'ReflectionClass::getReflectionConstant' => ['ReflectionClassConstant|false', 'name'=>'string'], 'ReflectionClass::getReflectionConstants' => ['list'], 'ReflectionClass::getShortName' => ['string'], -'ReflectionClass::getStartLine' => ['int|false'], +'ReflectionClass::getStartLine' => ['positive-int|false'], 'ReflectionClass::getStaticProperties' => ['array'], 'ReflectionClass::getStaticPropertyValue' => ['mixed', 'name'=>'string', 'default='=>'mixed'], 'ReflectionClass::getTraitAliases' => ['array'], @@ -10014,10 +9119,10 @@ 'ReflectionFunction::getClosureScopeClass' => ['ReflectionClass'], 'ReflectionFunction::getClosureThis' => ['bool'], 'ReflectionFunction::getDocComment' => ['string|false'], -'ReflectionFunction::getEndLine' => ['int|false'], +'ReflectionFunction::getEndLine' => ['positive-int|false'], 'ReflectionFunction::getExtension' => ['ReflectionExtension|null'], 'ReflectionFunction::getExtensionName' => ['string|false'], -'ReflectionFunction::getFileName' => ['string|false'], +'ReflectionFunction::getFileName' => ['non-empty-string|false'], 'ReflectionFunction::getName' => ['non-empty-string'], 'ReflectionFunction::getNamespaceName' => ['string'], 'ReflectionFunction::getNumberOfParameters' => ['int'], @@ -10025,7 +9130,7 @@ 'ReflectionFunction::getParameters' => ['list'], 'ReflectionFunction::getReturnType' => ['?ReflectionType'], 'ReflectionFunction::getShortName' => ['string'], -'ReflectionFunction::getStartLine' => ['int|false'], +'ReflectionFunction::getStartLine' => ['positive-int|false'], 'ReflectionFunction::getStaticVariables' => ['array'], 'ReflectionFunction::inNamespace' => ['bool'], 'ReflectionFunction::invoke' => ['mixed', '...args='=>'mixed'], @@ -10043,10 +9148,10 @@ 'ReflectionFunctionAbstract::getClosureScopeClass' => ['ReflectionClass|null'], 'ReflectionFunctionAbstract::getClosureThis' => ['object|null'], 'ReflectionFunctionAbstract::getDocComment' => ['string|false'], -'ReflectionFunctionAbstract::getEndLine' => ['int|false'], +'ReflectionFunctionAbstract::getEndLine' => ['positive-int|false'], 'ReflectionFunctionAbstract::getExtension' => ['ReflectionExtension|null'], 'ReflectionFunctionAbstract::getExtensionName' => ['string|false'], -'ReflectionFunctionAbstract::getFileName' => ['string|false'], +'ReflectionFunctionAbstract::getFileName' => ['non-empty-string|false'], 'ReflectionFunctionAbstract::getName' => ['non-empty-string'], 'ReflectionFunctionAbstract::getNamespaceName' => ['string'], 'ReflectionFunctionAbstract::getNumberOfParameters' => ['int'], @@ -10054,7 +9159,7 @@ 'ReflectionFunctionAbstract::getParameters' => ['list'], 'ReflectionFunctionAbstract::getReturnType' => ['?ReflectionType'], 'ReflectionFunctionAbstract::getShortName' => ['string'], -'ReflectionFunctionAbstract::getStartLine' => ['int|false'], +'ReflectionFunctionAbstract::getStartLine' => ['positive-int|false'], 'ReflectionFunctionAbstract::getStaticVariables' => ['array'], 'ReflectionFunctionAbstract::hasReturnType' => ['bool'], 'ReflectionFunctionAbstract::inNamespace' => ['bool'], @@ -10171,7 +9276,7 @@ 'ResourceBundle::get' => ['', 'index'=>'string|int', 'fallback='=>'bool'], 'ResourceBundle::getErrorCode' => ['int'], 'ResourceBundle::getErrorMessage' => ['string'], -'ResourceBundle::getLocales' => ['array', 'bundlename'=>'string'], +'ResourceBundle::getLocales' => ['array|false', 'bundlename'=>'string'], 'resourcebundle_count' => ['int', 'r'=>'resourcebundle'], 'resourcebundle_create' => ['?ResourceBundle', 'locale'=>'string', 'bundlename'=>'string', 'fallback='=>'bool'], 'resourcebundle_get' => ['', 'r'=>'resourcebundle', 'index'=>'string|int', 'fallback='=>'bool'], @@ -10285,7 +9390,7 @@ 'scalebarObj::set' => ['int', 'property_name'=>'string', 'new_value'=>''], 'scalebarObj::setImageColor' => ['int', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], 'scalebarObj::updateFromString' => ['int', 'snippet'=>'string'], -'scandir' => ['list|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'], @@ -10385,7 +9490,7 @@ 'session_destroy' => ['bool'], 'session_encode' => ['string|false'], 'session_gc' => ['int|false'], -'session_get_cookie_params' => ['array{lifetime:0|positive-int,path:non-falsy-string,domain:string,secure:bool,httponly:bool,samesite:string}'], +'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'], @@ -10402,7 +9507,7 @@ '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,samesite?:string}'], +'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'], @@ -10423,7 +9528,7 @@ 'SessionHandlerInterface::destroy' => ['bool', 'session_id'=>'string'], 'SessionHandlerInterface::gc' => ['int|false', 'maxlifetime'=>'int'], 'SessionHandlerInterface::open' => ['bool', 'save_path'=>'string', 'name'=>'string'], -'SessionHandlerInterface::read' => ['string', 'session_id'=>'string'], +'SessionHandlerInterface::read' => ['string|false', 'session_id'=>'string'], 'SessionHandlerInterface::write' => ['bool', 'session_id'=>'string', 'session_data'=>'string'], 'SessionIdInterface::create_sid' => ['string'], 'SessionUpdateTimestampHandler::updateTimestamp' => ['bool', 'id'=>'string', 'data'=>'string'], @@ -10442,16 +9547,13 @@ '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'], 'setrawcookie\'1' => ['bool', 'name'=>'string', 'value='=>'string', 'options='=>'array{ expires?:int, path?:string, domain?:string, secure?:bool, httponly?:bool, samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'}'], 'setRightFill' => ['void', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'a='=>'int'], 'setthreadtitle' => ['bool', 'title'=>'string'], 'settype' => ['bool', '&rw_var'=>'mixed', 'type'=>'string'], -'sha1' => ['non-falsy-string', 'str'=>'string', 'raw_output='=>'bool'], -'sha1_file' => ['non-falsy-string|false', 'filename'=>'string', 'raw_output='=>'bool'], -'sha256' => ['string', 'str'=>'string', 'raw_output='=>'bool'], -'sha256_file' => ['string', 'filename'=>'string', 'raw_output='=>'bool'], +'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'], @@ -10494,7 +9596,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'], @@ -10540,39 +9642,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'], @@ -10799,43 +9910,7 @@ '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'], @@ -11064,45 +10139,13 @@ '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'], @@ -11111,23 +10154,6 @@ 'SolrException::getPrevious' => ['Exception|Throwable'], 'SolrException::getTrace' => ['list\',args?:mixed[],object?:object}>'], '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'], @@ -11136,10 +10162,6 @@ 'SolrIllegalArgumentException::getPrevious' => ['Exception|Throwable'], 'SolrIllegalArgumentException::getTrace' => ['list\',args?:mixed[],object?:object}>'], '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'], @@ -11148,9 +10170,6 @@ 'SolrIllegalOperationException::getPrevious' => ['Exception|Throwable'], 'SolrIllegalOperationException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'SolrIllegalOperationException::getTraceAsString' => ['string'], -'SolrInputDocument::__clone' => ['void'], -'SolrInputDocument::__construct' => ['void'], -'SolrInputDocument::__destruct' => [''], 'SolrInputDocument::addChildDocument' => ['void', 'child'=>'SolrInputDocument'], 'SolrInputDocument::addChildDocuments' => ['void', 'docs'=>'array'], 'SolrInputDocument::addField' => ['bool', 'fieldname'=>'string', 'fieldvalue'=>'string', 'fieldboostvalue='=>'float'], @@ -11171,8 +10190,6 @@ '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'], @@ -11184,14 +10201,11 @@ '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'], @@ -11203,21 +10217,6 @@ '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'], @@ -11420,34 +10419,6 @@ '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'], @@ -11456,19 +10427,6 @@ 'SolrServerException::getPrevious' => ['Exception|Throwable'], 'SolrServerException::getTrace' => ['list\',args?:mixed[],object?:object}>'], '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'], @@ -11558,7 +10516,7 @@ 'SplFileInfo::getMTime' => ['__benevolent'], 'SplFileInfo::getOwner' => ['__benevolent'], 'SplFileInfo::getPath' => ['string'], -'SplFileInfo::getPathInfo' => ['SplFileInfo', 'class_name='=>'string'], +'SplFileInfo::getPathInfo' => ['__benevolent', 'class_name='=>'string'], 'SplFileInfo::getPathname' => ['string'], 'SplFileInfo::getPerms' => ['__benevolent'], 'SplFileInfo::getRealPath' => ['__benevolent'], @@ -11583,7 +10541,7 @@ 'SplFileObject::fgetcsv' => ['list|array{0: null}|false|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'SplFileObject::fgets' => ['string'], 'SplFileObject::fgetss' => ['string|false', 'allowable_tags='=>'string'], -'SplFileObject::flock' => ['bool', 'operation'=>'int', '&w_wouldblock='=>'int'], +'SplFileObject::flock' => ['bool', 'operation'=>'int-mask', '&w_wouldblock='=>'0|1'], 'SplFileObject::fpassthru' => ['int'], 'SplFileObject::fputcsv' => ['int|false', 'fields'=>'array', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'SplFileObject::fread' => ['string|false', 'length'=>'int'], @@ -11992,7 +10950,7 @@ 'strchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], 'strcmp' => ['int<-1, 1>', 'str1'=>'string', 'str2'=>'string'], 'strcoll' => ['int<-1, 1>', 'str1'=>'string', 'str2'=>'string'], -'strcspn' => ['int', 'str'=>'string', 'mask'=>'string', 'start='=>'int', 'length='=>'int'], +'strcspn' => ['non-negative-int', 'str'=>'string', 'mask'=>'string', 'start='=>'int', 'length='=>'int'], 'stream_bucket_append' => ['void', 'brigade'=>'resource', 'bucket'=>'object'], 'stream_bucket_make_writeable' => ['stdClass|null', 'brigade'=>'resource'], 'stream_bucket_new' => ['object', 'stream'=>'resource', 'buffer'=>'string'], @@ -12000,18 +10958,18 @@ '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_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'], @@ -12028,8 +10986,8 @@ 'stream_set_timeout' => ['bool', 'stream'=>'resource', 'seconds'=>'int', 'microseconds='=>'int'], 'stream_set_write_buffer' => ['int', 'fp'=>'resource', 'buffer'=>'int'], 'stream_socket_accept' => ['resource|false', 'serverstream'=>'resource', 'timeout='=>'float', '&w_peername='=>'string'], -'stream_socket_client' => ['resource|false', 'remoteaddress'=>'string', '&w_errcode='=>'int', '&w_errstring='=>'string', 'timeout='=>'float', 'flags='=>'int', 'context='=>'resource'], -'stream_socket_enable_crypto' => ['0|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'], @@ -12083,13 +11041,13 @@ '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' => ['non-empty-string|false', 'str'=>'string', 'token'=>'string'], 'strtok\'1' => ['non-empty-string|false', 'token'=>'string'], -'strtolower' => ['string', 'str'=>'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'=>'__stringAndStringable|int|float|bool|resource|null'], @@ -12475,7 +11433,7 @@ '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'], @@ -12608,7 +11566,7 @@ 'timezone_version_get' => ['string'], 'tmpfile' => ['__benevolent'], 'token_get_all' => ['list', 'source'=>'string', 'flags='=>'int'], -'token_name' => ['string', 'type'=>'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'], @@ -12665,169 +11623,169 @@ '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'], @@ -12934,7 +11892,7 @@ 'unserialize' => ['mixed', 'variable_representation'=>'string', 'allowed_classes='=>'array{allowed_classes?:string[]|bool}'], 'untaint' => ['bool', '&rw_string'=>'string', '&...rw_strings='=>'string'], 'uopz_add_function' => ['bool', 'class'=>'string', 'function'=>'string', 'handler'=>'Closure', '$flags'=>'bool', '$all'=>'bool'], -'uopz_add_function\1' => ['bool', 'function'=>'string', 'handler'=>'Closure', '$flags'=>'bool'], +'uopz_add_function\'1' => ['bool', 'function'=>'string', 'handler'=>'Closure', '$flags'=>'bool'], 'uopz_allow_exit' => ['void', 'allow'=>'bool'], 'uopz_backup' => ['void', 'class'=>'string', 'function'=>'string'], 'uopz_backup\'1' => ['void', 'function'=>'string'], @@ -12942,7 +11900,7 @@ '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'], @@ -12952,13 +11910,13 @@ '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'], @@ -12969,13 +11927,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'], @@ -13075,7 +12033,7 @@ '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'], +'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'], @@ -13274,7 +12232,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'], diff --git a/resources/functionMap_bleedingEdge.php b/resources/functionMap_bleedingEdge.php index 9f2230dd6f..92f41e9db0 100644 --- a/resources/functionMap_bleedingEdge.php +++ b/resources/functionMap_bleedingEdge.php @@ -2,148 +2,7 @@ return [ 'new' => [ - 'Closure::bind' => ['Closure', 'old'=>'Closure', 'to'=>'?object', 'scope='=>'object|class-string|\'static\'|null'], - 'Closure::bindTo' => ['Closure', 'new'=>'?object', 'newscope='=>'object|class-string|\'static\'|null'], - 'error_log' => ['bool', 'message'=>'string', 'message_type='=>'0|1|2|3|4', 'destination='=>'string', 'extra_headers='=>'string'], - 'SplFileObject::flock' => ['bool', 'operation'=>'int-mask', '&w_wouldblock='=>'0|1'], - 'Imagick::adaptiveBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::adaptiveSharpenImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::addNoiseImage' => ['bool', 'noise_type'=>'Imagick::NOISE_*', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::autoGammaImage' => ['bool', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::autoLevelImage' => ['bool', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::blurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::brightnessContrastImage' => ['bool', 'brightness'=>'float', 'contrast'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::clampImage' => ['bool', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::combineImages' => ['Imagick', 'channeltype'=>'Imagick::CHANNEL_*'], - 'Imagick::compareImageChannels' => ['array{Imagick,float}', 'image'=>'imagick', 'channeltype'=>'Imagick::CHANNEL_*', 'metrictype'=>'Imagick::METRIC_*'], - 'Imagick::compareImageLayers' => ['Imagick', 'method'=>'Imagick::LAYERMETHOD_*'], - 'Imagick::compareImages' => ['array{Imagick,float}', 'compare'=>'imagick', 'metric'=>'Imagick::METRIC_*'], - 'Imagick::compositeImage' => ['bool', 'composite_object'=>'imagick', 'composite'=>'Imagick::COMPOSITE_*', 'x'=>'int', 'y'=>'int', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::contrastStretchImage' => ['bool', 'black_point'=>'float', 'white_point'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::convolveImage' => ['bool', 'kernel'=>'array', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::distortImage' => ['bool', 'method'=>'Imagick::DISTORTION_*', 'arguments'=>'array', 'bestfit'=>'bool'], - 'Imagick::evaluateImage' => ['bool', 'op'=>'Imagick::EVALUATE_*', 'constant'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::exportImagePixels' => ['list', 'x'=>'int', 'y'=>'int', 'width'=>'int', 'height'=>'int', 'map'=>'string', 'storage'=>'Imagick::PIXEL_*'], - 'Imagick::floodFillPaintImage' => ['bool', 'fill'=>'mixed', 'fuzz'=>'float', 'target'=>'mixed', 'x'=>'int', 'y'=>'int', 'invert'=>'bool', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::functionImage' => ['bool', 'function'=>'Imagick::FUNCTION_*', 'arguments'=>'array', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::fxImage' => ['Imagick', 'expression'=>'string', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::gammaImage' => ['bool', 'gamma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::gaussianBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::getImageChannelDepth' => ['int', 'channel'=>'Imagick::CHANNEL_*'], - 'Imagick::getImageChannelDistortion' => ['float', 'reference'=>'imagick', 'channel'=>'Imagick::CHANNEL_*', 'metric'=>'Imagick::METRIC_*'], - 'Imagick::getImageChannelDistortions' => ['float', 'reference'=>'imagick', 'metric'=>'Imagick::METRIC_*', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::getImageChannelExtrema' => ['array{minima:0|positive-int,maxima:0|positive-int}', 'channel'=>'Imagick::CHANNEL_*'], - 'Imagick::getImageChannelKurtosis' => ['array{kurtosis:float,skewness:float}', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::getImageChannelMean' => ['array{mean:float,standardDeviation:float}', 'channel'=>'Imagick::CHANNEL_*'], - 'Imagick::getImageChannelRange' => ['array{minima:float,maxima:float}', 'channel'=>'Imagick::CHANNEL_*'], - 'Imagick::getImageDistortion' => ['float', 'reference'=>'magickwand', 'metric'=>'Imagick::METRIC_*'], - 'Imagick::getResource' => ['int', 'type'=>'Imagick::RESOURCETYPE_*'], - 'Imagick::getResourceLimit' => ['int', 'type'=>'Imagick::RESOURCETYPE_*'], - 'Imagick::importImagePixels' => ['bool', 'x'=>'int', 'y'=>'int', 'width'=>'int', 'height'=>'int', 'map'=>'string', 'storage'=>'Imagick::PIXEL_*', 'pixels'=>'array'], - 'Imagick::levelImage' => ['bool', 'blackpoint'=>'float', 'gamma'=>'float', 'whitepoint'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::mergeImageLayers' => ['Imagick', 'layer_method'=>'Imagick::LAYERMETHOD_*'], - 'Imagick::montageImage' => ['Imagick', 'draw'=>'imagickdraw', 'tile_geometry'=>'string', 'thumbnail_geometry'=>'string', 'mode'=>'Imagick::MONTAGEMODE_*', 'frame'=>'string'], - 'Imagick::morphology' => ['bool', 'morphologyMethod'=>'Imagick::MORPHOLOGY_*', 'iterations'=>'int', 'ImagickKernel'=>'ImagickKernel', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::motionBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'angle'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::negateImage' => ['bool', 'gray'=>'bool', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::normalizeImage' => ['bool', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::opaquePaintImage' => ['bool', 'target'=>'mixed', 'fill'=>'mixed', 'fuzz'=>'float', 'invert'=>'bool', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::orderedPosterizeImage' => ['bool', 'threshold_map'=>'string', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::paintFloodfillImage' => ['bool', 'fill'=>'mixed', 'fuzz'=>'float', 'bordercolor'=>'mixed', 'x'=>'int', 'y'=>'int', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::paintOpaqueImage' => ['bool', 'target'=>'mixed', 'fill'=>'mixed', 'fuzz'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::radialBlurImage' => ['bool', 'angle'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::randomThresholdImage' => ['bool', 'low'=>'float', 'high'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::remapImage' => ['bool', 'replacement'=>'imagick', 'dither'=>'Imagick::DITHERMETHOD_*'], - 'Imagick::rotationalBlurImage' => ['bool', 'float'=>'string', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::segmentImage' => ['bool', 'colorspace'=>'Imagick::COLORSPACE_*', 'cluster_threshold'=>'float', 'smooth_threshold'=>'float', 'verbose='=>'bool'], - 'Imagick::selectiveBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'threshold'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::separateImageChannel' => ['bool', 'channel'=>'Imagick::CHANNEL_*'], - 'Imagick::setColorspace' => ['bool', 'colorspace'=>'Imagick::COLORSPACE_*'], - 'Imagick::setCompression' => ['bool', 'compression'=>'Imagick::COMPRESSION_*'], - 'Imagick::setGravity' => ['bool', 'gravity'=>'Imagick::GRAVITY_*'], - 'Imagick::setImageAlphaChannel' => ['bool', 'mode'=>'Imagick::ALPHACHANNEL_*'], - 'Imagick::setImageChannelDepth' => ['bool', 'channel'=>'Imagick::CHANNEL_*', 'depth'=>'int'], - 'Imagick::setImageChannelMask' => ['', 'channel'=>'Imagick::CHANNEL_*'], - 'Imagick::setImageClipMask' => ['bool', 'clip_mask'=>'imagick'], - 'Imagick::setImageColormapColor' => ['bool', 'index'=>'int', 'color'=>'imagickpixel'], - 'Imagick::setImageColorspace' => ['bool', 'colorspace'=>'Imagick::COLORSPACE_*'], - 'Imagick::setImageCompose' => ['bool', 'compose'=>'Imagick::COMPOSITE_*'], - 'Imagick::setImageCompression' => ['bool', 'compression'=>'Imagick::COMPRESSION_*'], - 'Imagick::setImageDispose' => ['bool', 'dispose'=>'Imagick::DISPOSE_*'], - 'Imagick::setImageGravity' => ['bool', 'gravity'=>'Imagick::GRAVITY_*'], - 'Imagick::setImageInterlaceScheme' => ['bool', 'interlace_scheme'=>'Imagick::INTERLACE_*'], - 'Imagick::setImageInterpolateMethod' => ['bool', 'method'=>'Imagick::INTERPOLATE_*'], - 'Imagick::setImageOrientation' => ['bool', 'orientation'=>'Imagick::ORIENTATION_*'], - 'Imagick::setImageRenderingIntent' => ['bool', 'rendering_intent'=>'Imagick::RENDERINGINTENT_*'], - 'Imagick::setImageType' => ['bool', 'image_type'=>'Imagick::IMGTYPE_*'], - 'Imagick::setType' => ['bool', 'image_type'=>'Imagick::IMGTYPE_*'], - 'Imagick::sharpenImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::sigmoidalContrastImage' => ['bool', 'sharpen'=>'bool', 'alpha'=>'float', 'beta'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::similarityImage' => ['Imagick', 'imagick'=>'Imagick', '&bestMatch'=>'array', '&similarity'=>'float', 'similarity_threshold'=>'float', 'metric'=>'Imagick::METRIC_*'], - 'Imagick::sparseColorImage' => ['bool', 'sparse_method'=>'Imagick::SPARSECOLORMETHOD_*', 'arguments'=>'array', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::statisticImage' => ['bool', 'type'=>'Imagick::STATISTIC_*', 'width'=>'int', 'height'=>'int', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::thresholdImage' => ['bool', 'threshold'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'Imagick::transformImageColorspace' => ['bool', 'colorspace'=>'Imagick::COLORSPACE_*'], - 'Imagick::unsharpMaskImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'amount'=>'float', 'threshold'=>'float', 'channel='=>'Imagick::CHANNEL_*'], - 'ImagickDraw::color' => ['bool', 'x'=>'float', 'y'=>'float', 'paintmethod'=>'Imagick::PAINT_*'], - 'ImagickDraw::composite' => ['bool', 'compose'=>'Imagick::COMPOSITE_*', 'x'=>'float', 'y'=>'float', 'width'=>'float', 'height'=>'float', 'compositewand'=>'imagick'], - 'ImagickDraw::getFillRule' => ['Imagick::FILLRULE_*'], - 'ImagickDraw::getFontStretch' => ['Imagick::STRETCH_*'], - 'ImagickDraw::getFontStyle' => ['Imagick::STYLE_*'], - 'ImagickDraw::getGravity' => ['Imagick::GRAVITY_*'], - 'ImagickDraw::getStrokeLineCap' => ['Imagick::LINECAP_*'], - 'ImagickDraw::getStrokeLineJoin' => ['Imagick::LINEJOIN_*'], - 'ImagickDraw::getTextAlignment' => ['Imagick::ALIGN_*'], - 'ImagickDraw::getTextDecoration' => ['Imagick::DECORATION_*'], - 'ImagickDraw::matte' => ['bool', 'x'=>'float', 'y'=>'float', 'paintmethod'=>'Imagick::PAINT_*'], - 'ImagickDraw::setClipRule' => ['bool', 'fill_rule'=>'Imagick::FILLRULE_*'], - 'ImagickDraw::setFillRule' => ['bool', 'fill_rule'=>'Imagick::FILLRULE_*'], - 'ImagickDraw::setFontStretch' => ['bool', 'fontstretch'=>'Imagick::STRETCH_*'], - 'ImagickDraw::setFontStyle' => ['bool', 'style'=>'Imagick::STYLE_*'], - 'ImagickDraw::setGravity' => ['bool', 'gravity'=>'Imagick::GRAVITY_*'], - 'ImagickDraw::setStrokeLineCap' => ['bool', 'linecap'=>'Imagick::LINECAP_*'], - 'ImagickDraw::setStrokeLineJoin' => ['bool', 'linejoin'=>'Imagick::LINEJOIN_*'], - 'ImagickDraw::setTextAlignment' => ['bool', 'alignment'=>'Imagick::ALIGN_*'], - 'ImagickDraw::setTextAntialias' => ['bool', 'antialias'=>'bool'], - 'ImagickDraw::setTextDecoration' => ['bool', 'decoration'=>'Imagick::DECORATION_*'], - 'ImagickKernel::fromBuiltin' => ['ImagickKernel', 'kernelType'=>'Imagick::KERNEL_*', 'kernelString'=>'string'], - 'ImagickKernel::scale' => ['void', 'scale'=>'float', 'normalizeFlag'=>'Imagick::NORMALIZE_KERNEL_*'], - '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>'], - '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>'], - '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>'], - '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>'], - 'imagecreate' => ['__benevolent', 'x_size'=>'int<1, max>', 'y_size'=>'int<1, max>'], - 'imagecreatetruecolor' => ['__benevolent', 'x_size'=>'int<1, max>', 'y_size'=>'int<1, max>'], - 'max' => ['', '...arg1'=>'non-empty-array'], - 'mb_detect_order' => ['bool|list', 'encoding_list='=>'non-empty-list|non-falsy-string'], - 'min' => ['', '...arg1'=>'non-empty-array'], - 'file' => ['list|false', 'filename'=>'string', 'flags='=>'int-mask', 'context='=>'resource'], - 'flock' => ['bool', 'fp'=>'resource', 'operation'=>'int-mask', '&w_wouldblock='=>'0|1'], - 'ftp_append' => ['bool', 'ftp'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY'], - 'ftp_fget' => ['bool', 'stream'=>'resource', 'fp'=>'resource', 'remote_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'resumepos='=>'int'], - 'ftp_fput' => ['bool', 'stream'=>'resource', 'remote_file'=>'string', 'fp'=>'resource', 'mode='=>'FTP_ASCII|FTP_BINARY', 'startpos='=>'int'], - 'ftp_get' => ['bool', 'stream'=>'resource', 'local_file'=>'string', 'remote_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'resume_pos='=>'int'], - 'ftp_nb_fget' => ['int', 'stream'=>'resource', 'fp'=>'resource', 'remote_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'resumepos='=>'int'], - 'ftp_nb_fput' => ['int', 'stream'=>'resource', 'remote_file'=>'string', 'fp'=>'resource', 'mode='=>'FTP_ASCII|FTP_BINARY', 'startpos='=>'int'], - 'ftp_nb_get' => ['int|false', 'stream'=>'resource', 'local_file'=>'string', 'remote_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'resume_pos='=>'int'], - 'ftp_nb_put' => ['int|false', 'stream'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'startpos='=>'int'], - 'ftp_put' => ['bool', 'stream'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'startpos='=>'int'], - 'scandir' => ['list|false', 'dir'=>'string', 'sorting_order='=>'SCANDIR_SORT_ASCENDING|SCANDIR_SORT_DESCENDING| SCANDIR_SORT_NONE', 'context='=>'resource'], - 'stream_socket_client' => ['resource|false', 'remoteaddress'=>'string', '&w_errcode='=>'int', '&w_errstring='=>'string', 'timeout='=>'float', 'flags='=>'int-mask', 'context='=>'resource'], - 'stream_socket_enable_crypto' => ['0|bool', 'stream'=>'resource', 'enable'=>'bool', 'crypto_method='=>'STREAM_CRYPTO_METHOD_SSLv2_CLIENT|STREAM_CRYPTO_METHOD_SSLv3_CLIENT|STREAM_CRYPTO_METHOD_SSLv23_CLIENT|STREAM_CRYPTO_METHOD_ANY_CLIENT|STREAM_CRYPTO_METHOD_TLS_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT|STREAM_CRYPTO_METHOD_SSLv2_SERVER|STREAM_CRYPTO_METHOD_SSLv3_SERVER|STREAM_CRYPTO_METHOD_SSLv23_SERVER|STREAM_CRYPTO_METHOD_ANY_SERVER|STREAM_CRYPTO_METHOD_TLS_SERVER|STREAM_CRYPTO_METHOD_TLSv1_0_SERVER|STREAM_CRYPTO_METHOD_TLSv1_1_SERVER|STREAM_CRYPTO_METHOD_TLSv1_2_SERVER|STREAM_CRYPTO_METHOD_TLSv1_3_SERVER', 'session_stream='=>'resource'], - 'extract' => ['0|positive-int', '&rw_var_array'=>'array', 'extract_type='=>'EXTR_OVERWRITE|EXTR_SKIP|EXTR_PREFIX_SAME|EXTR_PREFIX_ALL|EXTR_PREFIX_INVALID|EXTR_IF_EXISTS|EXTR_PREFIX_IF_EXISTS|EXTR_REFS', 'prefix='=>'string|null'], - 'RecursiveIteratorIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator|IteratorAggregate', 'mode='=>'RecursiveIteratorIterator::LEAVES_ONLY|RecursiveIteratorIterator::SELF_FIRST|RecursiveIteratorIterator::CHILD_FIRST', 'flags='=>'0|RecursiveIteratorIterator::CATCH_GET_CHILD'], - '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_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}'], - 'count' => ['0|positive-int', 'var'=>'Countable|array', 'mode='=>'0|1'], ], 'old' => [ - - ] + ], ]; diff --git a/resources/functionMap_php74delta.php b/resources/functionMap_php74delta.php index b37fe5a7a5..9b187510c8 100644 --- a/resources/functionMap_php74delta.php +++ b/resources/functionMap_php74delta.php @@ -22,21 +22,21 @@ */ 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'], @@ -51,6 +51,7 @@ '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'], diff --git a/resources/functionMap_php80delta.php b/resources/functionMap_php80delta.php index 6ec7d665f7..ebf9aa9a1a 100644 --- a/resources/functionMap_php80delta.php +++ b/resources/functionMap_php80delta.php @@ -31,6 +31,7 @@ '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'], @@ -42,8 +43,12 @@ 'date_time_set' => ['DateTime', 'object'=>'DateTime', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], 'date_timestamp_set' => ['DateTime', 'object'=>'DateTime', 'unixtimestamp'=>'int'], 'date_timezone_set' => ['DateTime', 'object'=>'DateTime', 'timezone'=>'DateTimeZone'], + 'error_log' => ['bool', 'message'=>'string', 'message_type='=>'0|1|3|4', 'destination='=>'string', 'extra_headers='=>'string'], 'explode' => ['list', 'separator'=>'non-empty-string', 'str'=>'string', 'limit='=>'int'], 'fdiv' => ['float', 'dividend'=>'float', 'divisor'=>'float'], + 'fgetcsv' => ['list|array{0: null}|false', 'fp'=>'resource', 'length='=>'0|positive-int|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], + 'filter_input' => ['mixed', 'type'=>'INPUT_GET|INPUT_POST|INPUT_COOKIE|INPUT_SERVER|INPUT_ENV', 'variable_name'=>'string', 'filter='=>'int', 'options='=>'array|int'], + 'filter_input_array' => ['array|false|null', 'type'=>'INPUT_GET|INPUT_POST|INPUT_COOKIE|INPUT_SERVER|INPUT_ENV', 'definition='=>'int|array', 'add_empty='=>'bool'], 'floor' => ['float', 'number'=>'float'], 'forward_static_call_array' => ['mixed', 'function'=>'callable', 'parameters'=>'array'], 'get_debug_type' => ['string', 'var'=>'mixed'], @@ -51,11 +56,11 @@ 'gmdate' => ['string', 'format'=>'string', 'timestamp='=>'int'], 'gmmktime' => ['int|false', 'hour'=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], 'hash' => ['non-falsy-string', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], - 'hash_hkdf' => ['non-falsy-string', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], - 'hash_hmac' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], - 'hash_pbkdf2' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], + 'hash_hkdf' => ['non-falsy-string', 'algo'=>'non-falsy-string', 'key'=>'string', 'length='=>'0|positive-int', 'info='=>'string', 'salt='=>'string'], + 'hash_hmac' => ['non-falsy-string', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], + 'hash_pbkdf2' => ['non-falsy-string', 'algo'=>'non-falsy-string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'positive-int', 'length='=>'0|positive-int', 'raw_output='=>'bool'], 'imageaffine' => ['false|object', 'src'=>'resource', 'affine'=>'array', 'clip='=>'array'], - 'imagecreate' => ['__benevolent', 'width'=>'int', 'height'=>'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'], @@ -68,7 +73,7 @@ 'imagecreatefromwebp' => ['false|object', 'filename'=>'string'], 'imagecreatefromxbm' => ['false|object', 'filename'=>'string'], 'imagecreatefromxpm' => ['false|object', 'filename'=>'string'], - 'imagecreatetruecolor' => ['__benevolent', 'width'=>'int', 'height'=>'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'], @@ -77,26 +82,33 @@ '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_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' => ['non-empty-string', 'password'=>'string', 'algo'=>'string|int|null', 'options='=>'array'], 'PDOStatement::fetchAll' => ['array', 'how='=>'int', 'fetch_argument='=>'int|string|callable', 'ctor_args='=>'?array'], 'PhpToken::tokenize' => ['list', 'code'=>'string', 'flags='=>'int'], 'PhpToken::is' => ['bool', 'kind'=>'string|int|string[]|int[]'], 'PhpToken::isIgnorable' => ['bool'], - 'PhpToken::getTokenName' => ['string'], + 'PhpToken::getTokenName' => ['non-falsy-string'], 'preg_match_all' => ['0|positive-int|false', 'pattern'=>'string', 'subject'=>'string', '&w_subpatterns='=>'array', 'flags='=>'int', 'offset='=>'int'], 'proc_get_status' => ['array{command: string, pid: int, running: bool, signaled: bool, stopped: bool, exitcode: int, termsig: int, stopsig: int}', 'process'=>'resource'], + 'scandir' => ['__benevolent|false>', 'dir'=>'string', 'sorting_order='=>'SCANDIR_SORT_ASCENDING|SCANDIR_SORT_DESCENDING|SCANDIR_SORT_NONE', 'context='=>'?resource'], 'set_error_handler' => ['?callable', 'callback'=>'null|callable(int,string,string,int):bool', 'error_types='=>'int'], 'socket_addrinfo_lookup' => ['AddressInfo[]', 'node'=>'string', 'service='=>'mixed', 'hints='=>'array'], 'socket_select' => ['int|false', '&w_read'=>'Socket[]|null', '&w_write'=>'Socket[]|null', '&w_except'=>'Socket[]|null', 'seconds'=>'int|null', 'microseconds='=>'int'], 'sodium_crypto_aead_chacha20poly1305_ietf_decrypt' => ['string|false', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], + 'spl_autoload_functions' => ['list'], 'str_contains' => ['bool', 'haystack'=>'string', 'needle'=>'string'], 'str_split' => ['non-empty-list', 'str'=>'string', 'split_length='=>'positive-int'], 'str_ends_with' => ['bool', 'haystack'=>'string', 'needle'=>'string'], @@ -111,7 +123,7 @@ 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], 'substr' => ['string', 'string'=>'string', 'start'=>'int', 'length='=>'int'], 'round' => ['float', 'number'=>'float', 'precision='=>'int', 'mode='=>'1|2|3|4'], - 'version_compare' => ['int|bool', 'version1'=>'string', 'version2'=>'string', 'operator='=>'string'], + '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'], @@ -169,6 +181,7 @@ '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'], @@ -189,10 +202,10 @@ 'gmmktime' => ['int|false', 'hour='=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], 'gmp_random' => ['GMP', 'limiter='=>'int'], 'gzgetss' => ['string|false', 'zp'=>'resource', 'length'=>'int', 'allowable_tags='=>'string'], - 'hash' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], - 'hash_hkdf' => ['non-empty-string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], - 'hash_hmac' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], - 'hash_pbkdf2' => ['non-empty-string|false', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], + 'hash' => ['non-falsy-string|false', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], + 'hash_hkdf' => ['non-falsy-string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + 'hash_hmac' => ['non-falsy-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], + 'hash_pbkdf2' => ['non-falsy-string|false', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], 'hebrevc' => ['string', 'str'=>'string', 'max_chars_per_line='=>'int'], 'image2wbmp' => ['bool', 'im'=>'resource', 'filename='=>'?string', 'threshold='=>'int'], 'imageaffine' => ['resource|false', 'src'=>'resource', 'affine'=>'array', 'clip='=>'array'], @@ -249,7 +262,7 @@ 'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int', 'before_needle='=>'bool'], 'substr' => ['__benevolent', 'string'=>'string', 'start'=>'int', 'length='=>'int'], - 'version_compare' => ['int|bool', 'version1'=>'string', 'version2'=>'string', 'operator='=>'string'], + 'version_compare' => ['int|bool', 'version1'=>'string', 'version2'=>'string', 'operator='=>'string|null'], 'xml_parser_create' => ['resource', 'encoding='=>'string'], 'xml_parser_create_ns' => ['resource', 'encoding='=>'string', 'sep='=>'string'], 'xml_parser_free' => ['bool', 'parser'=>'resource'], diff --git a/resources/functionMap_php80delta_bleedingEdge.php b/resources/functionMap_php80delta_bleedingEdge.php index 82e9b720a8..92f41e9db0 100644 --- a/resources/functionMap_php80delta_bleedingEdge.php +++ b/resources/functionMap_php80delta_bleedingEdge.php @@ -2,16 +2,7 @@ return [ 'new' => [ - 'error_log' => ['bool', 'message'=>'string', 'message_type='=>'0|1|3|4', 'destination='=>'string', 'extra_headers='=>'string'], - 'filter_input' => ['mixed', 'type'=>'INPUT_GET|INPUT_POST|INPUT_COOKIE|INPUT_SERVER|INPUT_ENV', 'variable_name'=>'string', 'filter='=>'int', 'options='=>'array|int'], - 'filter_input_array' => ['array|false|null', 'type'=>'INPUT_GET|INPUT_POST|INPUT_COOKIE|INPUT_SERVER|INPUT_ENV', 'definition='=>'int|array', 'add_empty='=>'bool'], - 'hash_hkdf' => ['non-falsy-string', 'algo'=>'non-falsy-string', 'key'=>'string', 'length='=>'0|positive-int', 'info='=>'string', 'salt='=>'string'], - 'hash_pbkdf2' => ['non-empty-string', 'algo'=>'non-falsy-string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'positive-int', 'length='=>'0|positive-int', 'raw_output='=>'bool'], - 'imagecreate' => ['__benevolent', 'width'=>'int<1, max>', 'height'=>'int<1, max>'], - 'imagecreatetruecolor' => ['__benevolent', 'width'=>'int<1, max>', 'height'=>'int<1, max>'], - 'mb_detect_order' => ['bool|list', 'encoding_list='=>'non-empty-list|non-falsy-string|null'], ], 'old' => [ - - ] + ], ]; diff --git a/resources/functionMap_php81delta.php b/resources/functionMap_php81delta.php index 54a3199934..2fca7f239b 100644 --- a/resources/functionMap_php81delta.php +++ b/resources/functionMap_php81delta.php @@ -50,8 +50,8 @@ 'pg_field_prtlen\'1' => ['int', 'result'=>'', 'row'=>'int', 'field_name_or_number'=>'string|int'], 'pg_lo_export' => ['bool', 'connection'=>'resource', 'oid'=>'int', 'filename'=>'string'], 'pg_lo_export\'1' => ['bool', 'oid'=>'int', 'pathname'=>'string'], - 'pg_lo_import' => ['int|false', 'connection'=>'resource', 'pathname'=>'string', 'oid'=>''], - 'pg_lo_import\'1' => ['int', 'pathname'=>'string', 'oid'=>''], + 'pg_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'], diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index 4ba615393d..97f858cc49 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -86,6 +86,11 @@ '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], @@ -634,6 +639,8 @@ '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], @@ -646,12 +653,24 @@ '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], 'StringBackedEnum::from' => ['hasSideEffects' => false], 'StringBackedEnum::tryFrom' => ['hasSideEffects' => false], 'StubTests\\CodeStyle\\BracesOneLineFixer::getDefinition' => ['hasSideEffects' => false], 'StubTests\\Parsers\\ExpectedFunctionArgumentsInfo::__toString' => ['hasSideEffects' => false], - 'StubTests\\Parsers\\Visitors\\CoreStubASTVisitor::__construct' => ['hasSideEffects' => false], 'StubTests\\StubsMetaExpectedArgumentsTest::getClassMemberFqn' => ['hasSideEffects' => false], 'StubTests\\StubsParameterNamesTest::printParameters' => ['hasSideEffects' => false], 'Transliterator::createInverse' => ['hasSideEffects' => false], @@ -777,11 +796,11 @@ '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], @@ -789,7 +808,7 @@ 'compact' => ['hasSideEffects' => false], 'connection_aborted' => ['hasSideEffects' => true], 'connection_status' => ['hasSideEffects' => true], - 'constant' => ['hasSideEffects' => false], + 'constant' => ['hasSideEffects' => true], 'convert_cyr_string' => ['hasSideEffects' => false], 'convert_uudecode' => ['hasSideEffects' => false], 'convert_uuencode' => ['hasSideEffects' => false], @@ -812,47 +831,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], @@ -860,19 +879,20 @@ 'datefmt_get_timezone_id' => ['hasSideEffects' => false], 'datefmt_is_lenient' => ['hasSideEffects' => false], 'dcngettext' => ['hasSideEffects' => false], + 'debug_backtrace' => ['hasSideEffects' => true], 'decbin' => ['hasSideEffects' => false], 'dechex' => ['hasSideEffects' => false], 'decoct' => ['hasSideEffects' => false], - 'defined' => ['hasSideEffects' => false], + 'defined' => ['hasSideEffects' => true], 'deflate_init' => ['hasSideEffects' => false], 'deg2rad' => ['hasSideEffects' => false], 'dirname' => ['hasSideEffects' => false], - 'disk_free_space' => ['hasSideEffects' => false], - 'disk_total_space' => ['hasSideEffects' => false], - 'diskfreespace' => ['hasSideEffects' => false], + 'disk_free_space' => ['hasSideEffects' => true], + 'disk_total_space' => ['hasSideEffects' => true], + 'diskfreespace' => ['hasSideEffects' => true], 'dngettext' => ['hasSideEffects' => false], 'doubleval' => ['hasSideEffects' => false], - 'error_get_last' => ['hasSideEffects' => false], + 'error_get_last' => ['hasSideEffects' => true], 'error_log' => ['hasSideEffects' => true], 'escapeshellarg' => ['hasSideEffects' => false], 'escapeshellcmd' => ['hasSideEffects' => false], @@ -882,13 +902,13 @@ 'extension_loaded' => ['hasSideEffects' => false], 'fclose' => ['hasSideEffects' => true], 'fdiv' => ['hasSideEffects' => false], - 'feof' => ['hasSideEffects' => false], + 'feof' => ['hasSideEffects' => true], 'fflush' => ['hasSideEffects' => true], 'fgetc' => ['hasSideEffects' => true], 'fgetcsv' => ['hasSideEffects' => true], 'fgets' => ['hasSideEffects' => true], 'fgetss' => ['hasSideEffects' => true], - 'file' => ['hasSideEffects' => false], + 'file' => ['hasSideEffects' => true], 'file_exists' => ['hasSideEffects' => false], 'file_get_contents' => ['hasSideEffects' => true], 'file_put_contents' => ['hasSideEffects' => true], @@ -914,7 +934,7 @@ 'flock' => ['hasSideEffects' => true], 'floor' => ['hasSideEffects' => false], 'fmod' => ['hasSideEffects' => false], - 'fnmatch' => ['hasSideEffects' => false], + 'fnmatch' => ['hasSideEffects' => true], 'fopen' => ['hasSideEffects' => true], 'fpassthru' => ['hasSideEffects' => true], 'fputcsv' => ['hasSideEffects' => true], @@ -922,17 +942,17 @@ 'fread' => ['hasSideEffects' => true], 'fscanf' => ['hasSideEffects' => true], 'fseek' => ['hasSideEffects' => true], - 'fstat' => ['hasSideEffects' => false], + 'fstat' => ['hasSideEffects' => true], 'ftell' => ['hasSideEffects' => false], - 'ftok' => ['hasSideEffects' => false], + 'ftok' => ['hasSideEffects' => true], 'ftruncate' => ['hasSideEffects' => true], 'func_get_arg' => ['hasSideEffects' => false], 'func_get_args' => ['hasSideEffects' => false], 'func_num_args' => ['hasSideEffects' => false], 'function_exists' => ['hasSideEffects' => false], 'fwrite' => ['hasSideEffects' => true], - 'gc_enabled' => ['hasSideEffects' => false], - 'gc_status' => ['hasSideEffects' => false], + 'gc_enabled' => ['hasSideEffects' => true], + 'gc_status' => ['hasSideEffects' => true], 'gd_info' => ['hasSideEffects' => false], 'geoip_continent_code_by_name' => ['hasSideEffects' => false], 'geoip_country_code3_by_name' => ['hasSideEffects' => false], @@ -949,57 +969,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], @@ -1074,7 +1096,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], @@ -1112,7 +1134,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], @@ -1147,13 +1169,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], @@ -1166,8 +1188,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], @@ -1176,7 +1198,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], @@ -1203,8 +1225,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], @@ -1246,7 +1268,7 @@ '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], @@ -1259,10 +1281,10 @@ 'lcfirst' => ['hasSideEffects' => false], 'lchgrp' => ['hasSideEffects' => true], 'lchown' => ['hasSideEffects' => true], - 'libxml_get_errors' => ['hasSideEffects' => false], - 'libxml_get_last_error' => ['hasSideEffects' => false], + 'libxml_get_errors' => ['hasSideEffects' => true], + 'libxml_get_last_error' => ['hasSideEffects' => true], 'link' => ['hasSideEffects' => true], - 'linkinfo' => ['hasSideEffects' => false], + 'linkinfo' => ['hasSideEffects' => true], 'locale_accept_from_http' => ['hasSideEffects' => false], 'locale_canonicalize' => ['hasSideEffects' => false], 'locale_compose' => ['hasSideEffects' => false], @@ -1280,8 +1302,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], @@ -1337,9 +1359,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], @@ -1347,16 +1369,16 @@ 'mhash_get_block_size' => ['hasSideEffects' => false], 'mhash_get_hash_name' => ['hasSideEffects' => false], 'mhash_keygen_s2k' => ['hasSideEffects' => false], - 'microtime' => ['hasSideEffects' => false], + 'microtime' => ['hasSideEffects' => true], 'min' => ['hasSideEffects' => false], 'mkdir' => ['hasSideEffects' => true], - 'mktime' => ['hasSideEffects' => false], + 'mktime' => ['hasSideEffects' => true], 'move_uploaded_file' => ['hasSideEffects' => true], 'msgfmt_create' => ['hasSideEffects' => false], 'msgfmt_format' => ['hasSideEffects' => false], 'msgfmt_format_message' => ['hasSideEffects' => false], - 'msgfmt_get_error_code' => ['hasSideEffects' => false], - 'msgfmt_get_error_message' => ['hasSideEffects' => false], + 'msgfmt_get_error_code' => ['hasSideEffects' => true], + 'msgfmt_get_error_message' => ['hasSideEffects' => true], 'msgfmt_get_locale' => ['hasSideEffects' => false], 'msgfmt_get_pattern' => ['hasSideEffects' => false], 'msgfmt_parse' => ['hasSideEffects' => false], @@ -1366,7 +1388,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], @@ -1375,28 +1397,40 @@ 'numfmt_format' => ['hasSideEffects' => false], 'numfmt_format_currency' => ['hasSideEffects' => false], 'numfmt_get_attribute' => ['hasSideEffects' => false], - 'numfmt_get_error_code' => ['hasSideEffects' => false], - 'numfmt_get_error_message' => ['hasSideEffects' => false], + 'numfmt_get_error_code' => ['hasSideEffects' => true], + 'numfmt_get_error_message' => ['hasSideEffects' => true], 'numfmt_get_locale' => ['hasSideEffects' => false], 'numfmt_get_pattern' => ['hasSideEffects' => false], 'numfmt_get_symbol' => ['hasSideEffects' => false], 'numfmt_get_text_attribute' => ['hasSideEffects' => false], 'numfmt_parse' => ['hasSideEffects' => false], + 'ob_clean' => ['hasSideEffects' => true], + 'ob_end_clean' => ['hasSideEffects' => true], + 'ob_end_flush' => ['hasSideEffects' => true], 'ob_etaghandler' => ['hasSideEffects' => false], - 'ob_get_contents' => ['hasSideEffects' => false], + 'ob_flush' => ['hasSideEffects' => true], + 'ob_get_clean' => ['hasSideEffects' => true], + 'ob_get_contents' => ['hasSideEffects' => true], + 'ob_get_flush' => ['hasSideEffects' => true], + 'ob_get_length' => ['hasSideEffects' => true], + 'ob_get_level' => ['hasSideEffects' => true], + 'ob_get_status' => ['hasSideEffects' => true], 'ob_iconv_handler' => ['hasSideEffects' => false], + 'ob_list_handlers' => ['hasSideEffects' => true], 'octdec' => ['hasSideEffects' => false], 'ord' => ['hasSideEffects' => false], + 'output_add_rewrite_var' => ['hasSideEffects' => true], + 'output_reset_rewrite_vars' => ['hasSideEffects' => true], 'pack' => ['hasSideEffects' => false], 'pam_auth' => ['hasSideEffects' => false], 'pam_chpass' => ['hasSideEffects' => false], - 'parse_ini_file' => ['hasSideEffects' => false], + 'parse_ini_file' => ['hasSideEffects' => true], 'parse_ini_string' => ['hasSideEffects' => false], 'parse_url' => ['hasSideEffects' => false], - 'pathinfo' => ['hasSideEffects' => false], + 'pathinfo' => ['hasSideEffects' => true], 'pclose' => ['hasSideEffects' => true], - 'pcntl_errno' => ['hasSideEffects' => false], - 'pcntl_get_last_error' => ['hasSideEffects' => false], + 'pcntl_errno' => ['hasSideEffects' => true], + 'pcntl_get_last_error' => ['hasSideEffects' => true], 'pcntl_getpriority' => ['hasSideEffects' => false], 'pcntl_strerror' => ['hasSideEffects' => false], 'pcntl_wexitstatus' => ['hasSideEffects' => false], @@ -1411,16 +1445,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], @@ -1445,8 +1479,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], @@ -1461,23 +1495,23 @@ 'rawurldecode' => ['hasSideEffects' => false], 'rawurlencode' => ['hasSideEffects' => false], 'readfile' => ['hasSideEffects' => true], - 'readlink' => ['hasSideEffects' => false], - 'realpath' => ['hasSideEffects' => false], - 'realpath_cache_get' => ['hasSideEffects' => false], - 'realpath_cache_size' => ['hasSideEffects' => false], + 'readlink' => ['hasSideEffects' => true], + 'realpath' => ['hasSideEffects' => true], + 'realpath_cache_get' => ['hasSideEffects' => true], + 'realpath_cache_size' => ['hasSideEffects' => true], 'rename' => ['hasSideEffects' => true], 'resourcebundle_count' => ['hasSideEffects' => false], 'resourcebundle_create' => ['hasSideEffects' => false], 'resourcebundle_get' => ['hasSideEffects' => false], - 'resourcebundle_get_error_code' => ['hasSideEffects' => false], - 'resourcebundle_get_error_message' => ['hasSideEffects' => false], + 'resourcebundle_get_error_code' => ['hasSideEffects' => true], + 'resourcebundle_get_error_message' => ['hasSideEffects' => true], 'resourcebundle_locales' => ['hasSideEffects' => false], 'rewind' => ['hasSideEffects' => true], 'rmdir' => ['hasSideEffects' => true], 'round' => ['hasSideEffects' => false], 'rtrim' => ['hasSideEffects' => false], 'sha1' => ['hasSideEffects' => false], - 'sha1_file' => ['hasSideEffects' => false], + 'sha1_file' => ['hasSideEffects' => true], 'sin' => ['hasSideEffects' => false], 'sinh' => ['hasSideEffects' => false], 'sizeof' => ['hasSideEffects' => false], @@ -1503,9 +1537,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], @@ -1520,7 +1554,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], @@ -1528,7 +1562,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], @@ -1537,18 +1571,18 @@ 'substr_count' => ['hasSideEffects' => false], 'substr_replace' => ['hasSideEffects' => false], 'symlink' => ['hasSideEffects' => true], - 'sys_getloadavg' => ['hasSideEffects' => false], + 'sys_getloadavg' => ['hasSideEffects' => true], 'tan' => ['hasSideEffects' => false], 'tanh' => ['hasSideEffects' => false], 'tempnam' => ['hasSideEffects' => true], 'timezone_abbreviations_list' => ['hasSideEffects' => false], - 'timezone_identifiers_list' => ['hasSideEffects' => false], - 'timezone_location_get' => ['hasSideEffects' => false], - 'timezone_name_from_abbr' => ['hasSideEffects' => false], + 'timezone_identifiers_list' => ['hasSideEffects' => true], + 'timezone_location_get' => ['hasSideEffects' => true], + 'timezone_name_from_abbr' => ['hasSideEffects' => true], 'timezone_name_get' => ['hasSideEffects' => false], - 'timezone_offset_get' => ['hasSideEffects' => false], - 'timezone_open' => ['hasSideEffects' => false], - 'timezone_transitions_get' => ['hasSideEffects' => false], + 'timezone_offset_get' => ['hasSideEffects' => true], + 'timezone_open' => ['hasSideEffects' => true], + 'timezone_transitions_get' => ['hasSideEffects' => true], 'timezone_version_get' => ['hasSideEffects' => false], 'tmpfile' => ['hasSideEffects' => true], 'token_get_all' => ['hasSideEffects' => false], @@ -1557,20 +1591,22 @@ 'transliterator_create' => ['hasSideEffects' => false], 'transliterator_create_from_rules' => ['hasSideEffects' => false], 'transliterator_create_inverse' => ['hasSideEffects' => false], - 'transliterator_get_error_code' => ['hasSideEffects' => false], - 'transliterator_get_error_message' => ['hasSideEffects' => false], + 'transliterator_get_error_code' => ['hasSideEffects' => true], + 'transliterator_get_error_message' => ['hasSideEffects' => true], 'transliterator_list_ids' => ['hasSideEffects' => false], 'transliterator_transliterate' => ['hasSideEffects' => false], 'trim' => ['hasSideEffects' => false], 'ucfirst' => ['hasSideEffects' => false], 'ucwords' => ['hasSideEffects' => false], 'umask' => ['hasSideEffects' => true], + 'uniqid' => ['hasSideEffects' => true], 'unlink' => ['hasSideEffects' => true], 'unpack' => ['hasSideEffects' => false], 'urldecode' => ['hasSideEffects' => false], 'urlencode' => ['hasSideEffects' => false], 'utf8_decode' => ['hasSideEffects' => false], 'utf8_encode' => ['hasSideEffects' => false], + 'version_compare' => ['hasSideEffects' => false], 'vsprintf' => ['hasSideEffects' => false], 'wordwrap' => ['hasSideEffects' => false], 'xml_error_string' => ['hasSideEffects' => false], diff --git a/src/Analyser/Analyser.php b/src/Analyser/Analyser.php index ed161671c6..e8f239ba84 100644 --- a/src/Analyser/Analyser.php +++ b/src/Analyser/Analyser.php @@ -5,6 +5,8 @@ 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; @@ -12,7 +14,11 @@ use function count; use function memory_get_peak_usage; -class Analyser +/** + * @phpstan-import-type CollectorData from CollectedData + */ +#[AutowiredService] +final class Analyser { public function __construct( @@ -20,6 +26,7 @@ public function __construct( private RuleRegistry $ruleRegistry, private CollectorRegistry $collectorRegistry, private NodeScopeResolver $nodeScopeResolver, + #[AutowiredParameter] private int $internalErrorsCountLimit, ) { @@ -59,12 +66,13 @@ public function analyse( $linesToIgnore = []; $unmatchedLineIgnores = []; - /** @var list $collectedData */ + /** @var CollectorData $collectedData */ $collectedData = []; $internalErrorsCount = 0; $reachedInternalErrorsCountLimit = false; $dependencies = []; + $usedTraitDependencies = []; $exportedNodes = []; foreach ($files as $file) { if ($preFileCallback !== null) { @@ -88,6 +96,7 @@ public function analyse( $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) { @@ -98,7 +107,7 @@ public function analyse( throw $t; } $internalErrorsCount++; - $errors[] = (new Error($t->getMessage(), $file, null, $t)) + $errors[] = (new Error($t->getMessage(), $file, canBeIgnored: $t)) ->withIdentifier('phpstan.internal') ->withMetadata([ InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($t), @@ -127,6 +136,7 @@ public function analyse( [], $collectedData, $internalErrorsCount === 0 ? $dependencies : null, + $internalErrorsCount === 0 ? $usedTraitDependencies : null, $exportedNodes, $reachedInternalErrorsCountLimit, memory_get_peak_usage(true), diff --git a/src/Analyser/AnalyserResult.php b/src/Analyser/AnalyserResult.php index 903ab9c5dc..576471d1ff 100644 --- a/src/Analyser/AnalyserResult.php +++ b/src/Analyser/AnalyserResult.php @@ -8,8 +8,9 @@ /** * @phpstan-import-type LinesToIgnore from FileAnalyserResult + * @phpstan-import-type CollectorData from CollectedData */ -class AnalyserResult +final class AnalyserResult { /** @var list|null */ @@ -22,9 +23,10 @@ class AnalyserResult * @param list $locallyIgnoredErrors * @param array $linesToIgnore * @param array $unmatchedLineIgnores - * @param list $collectedData + * @param CollectorData $collectedData * @param list $internalErrors * @param array>|null $dependencies + * @param array>|null $usedTraitDependencies * @param array> $exportedNodes */ public function __construct( @@ -37,6 +39,7 @@ public function __construct( private array $internalErrors, private array $collectedData, private ?array $dependencies, + private ?array $usedTraitDependencies, private array $exportedNodes, private bool $reachedInternalErrorsCountLimit, private int $peakMemoryUsageBytes, @@ -125,7 +128,7 @@ public function getInternalErrors(): array } /** - * @return list + * @return CollectorData */ public function getCollectedData(): array { @@ -140,6 +143,14 @@ public function getDependencies(): ?array return $this->dependencies; } + /** + * @return array>|null + */ + public function getUsedTraitDependencies(): ?array + { + return $this->usedTraitDependencies; + } + /** * @return array> */ diff --git a/src/Analyser/AnalyserResultFinalizer.php b/src/Analyser/AnalyserResultFinalizer.php index 707ac02005..97c3bdfd1c 100644 --- a/src/Analyser/AnalyserResultFinalizer.php +++ b/src/Analyser/AnalyserResultFinalizer.php @@ -6,6 +6,8 @@ use PHPStan\BetterReflection\NodeCompiler\Exception\UnableToCompileNode; use PHPStan\BetterReflection\Reflection\Exception\CircularReference; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\CollectedDataNode; use PHPStan\Rules\Registry as RuleRegistry; use Throwable; @@ -14,14 +16,17 @@ use function get_class; use function sprintf; -class AnalyserResultFinalizer +#[AutowiredService] +final class AnalyserResultFinalizer { public function __construct( private RuleRegistry $ruleRegistry, + private IgnoreErrorExtensionProvider $ignoreErrorExtensionProvider, private RuleErrorTransformer $ruleErrorTransformer, private ScopeFactory $scopeFactory, private LocalIgnoresProcessor $localIgnoresProcessor, + #[AutowiredParameter] private bool $reportUnmatchedIgnoredErrors, ) { @@ -49,7 +54,7 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $ try { $ruleErrors = $rule->processNode($node, $scope); } catch (AnalysedCodeException $e) { - $tempCollectorErrors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $e, null, null, $e->getTip())) + $tempCollectorErrors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $e, tip: $e->getTip())) ->withIdentifier('phpstan.internal') ->withMetadata([ InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), @@ -57,7 +62,7 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $ ]); continue; } catch (IdentifierNotFound $e) { - $tempCollectorErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getStartLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols')) + $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), @@ -88,7 +93,17 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $ } foreach ($ruleErrors as $ruleError) { - $tempCollectorErrors[] = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine()); + $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; } } @@ -129,6 +144,7 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $ $internalErrors, $analyserResult->getCollectedData(), $analyserResult->getDependencies(), + $analyserResult->getUsedTraitDependencies(), $analyserResult->getExportedNodes(), $analyserResult->hasReachedInternalErrorsCountLimit(), $analyserResult->getPeakMemoryUsageBytes(), @@ -147,6 +163,7 @@ private function mergeFilteredPhpErrors(AnalyserResult $analyserResult): Analyse $analyserResult->getInternalErrors(), $analyserResult->getCollectedData(), $analyserResult->getDependencies(), + $analyserResult->getUsedTraitDependencies(), $analyserResult->getExportedNodes(), $analyserResult->hasReachedInternalErrorsCountLimit(), $analyserResult->getPeakMemoryUsageBytes(), @@ -210,6 +227,7 @@ private function addUnmatchedIgnoredErrors( $analyserResult->getInternalErrors(), $analyserResult->getCollectedData(), $analyserResult->getDependencies(), + $analyserResult->getUsedTraitDependencies(), $analyserResult->getExportedNodes(), $analyserResult->hasReachedInternalErrorsCountLimit(), $analyserResult->getPeakMemoryUsageBytes(), diff --git a/src/Analyser/ArgumentsNormalizer.php b/src/Analyser/ArgumentsNormalizer.php index 6c6db632ac..4a4844cd8c 100644 --- a/src/Analyser/ArgumentsNormalizer.php +++ b/src/Analyser/ArgumentsNormalizer.php @@ -11,12 +11,14 @@ use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; use function array_key_exists; use function array_keys; use function count; use function ksort; use function max; +use function sprintf; /** * @api @@ -27,7 +29,7 @@ final class ArgumentsNormalizer public const ORIGINAL_ARG_ATTRIBUTE = 'originalArg'; /** - * @return array{ParametersAcceptor, FuncCall}|null + * @return array{ParametersAcceptor, FuncCall, TrinaryLogic}|null */ public static function reorderCallUserFuncArguments( FuncCall $callUserFuncCall, @@ -65,18 +67,24 @@ public static function reorderCallUserFuncArguments( return null; } + $callableParametersAcceptors = $calledOnType->getCallableParametersAcceptors($scope); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $passThruArgs, - $calledOnType->getCallableParametersAcceptors($scope), + $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( @@ -90,6 +98,11 @@ public static function reorderFuncArguments( 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, @@ -108,6 +121,11 @@ public static function reorderMethodArguments( 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, @@ -127,6 +145,11 @@ public static function reorderStaticCallArguments( 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, @@ -146,6 +169,11 @@ public static function reorderNewArguments( 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, @@ -194,7 +222,16 @@ public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array foreach ($callArgs as $i => $arg) { if ($arg->name === null) { // add regular args as is - $reorderedArgs[$i] = $arg; + + $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 @@ -269,7 +306,7 @@ public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array $defaultValue = $parameter->getDefaultValue(); if ($defaultValue === null) { if (!$parameter->isVariadic()) { - throw new ShouldNotHappenException('An optional parameter must have a default value'); + throw new ShouldNotHappenException(sprintf('An optional parameter $%s must have a default value', $parameter->getName())); } $defaultValue = new ConstantArrayType([], []); } diff --git a/src/Analyser/ConditionalExpressionHolder.php b/src/Analyser/ConditionalExpressionHolder.php index 561feac059..6907183966 100644 --- a/src/Analyser/ConditionalExpressionHolder.php +++ b/src/Analyser/ConditionalExpressionHolder.php @@ -8,7 +8,7 @@ use function implode; use function sprintf; -class ConditionalExpressionHolder +final class ConditionalExpressionHolder { /** diff --git a/src/Analyser/ConstantResolver.php b/src/Analyser/ConstantResolver.php index d8df652c42..cf05925fa1 100644 --- a/src/Analyser/ConstantResolver.php +++ b/src/Analyser/ConstantResolver.php @@ -3,9 +3,13 @@ namespace PHPStan\Analyser; use PhpParser\Node\Name; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\ComposerPhpVersionFactory; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\NamespaceAnswerer; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -20,21 +24,33 @@ use PHPStan\Type\UnionType; use function array_key_exists; use function in_array; +use function is_array; +use function is_int; +use function max; use function sprintf; use const INF; use const NAN; use const PHP_INT_SIZE; -class ConstantResolver +#[AutowiredService(factory: '@PHPStan\Analyser\ConstantResolverFactory::create')] +final class ConstantResolver { + public const PHP_MIN_ANALYZABLE_VERSION_ID = 50207; + /** @var array */ private array $currentlyResolving = []; /** * @param string[] $dynamicConstantNames + * @param int|array{min: int, max: int}|null $phpVersion */ - public function __construct(private ReflectionProviderProvider $reflectionProviderProvider, private array $dynamicConstantNames) + public function __construct( + private ReflectionProviderProvider $reflectionProviderProvider, + private array $dynamicConstantNames, + private int|array|null $phpVersion, + private ComposerPhpVersionFactory $composerPhpVersionFactory, + ) { } @@ -76,17 +92,69 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type 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') { - return IntegerRangeType::fromInterval(5, null); + $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') { - return IntegerRangeType::fromInterval(0, null); + $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') { - return IntegerRangeType::fromInterval(0, null); + $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') { - return IntegerRangeType::fromInterval(50207, null); + $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([ @@ -300,6 +368,40 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type return null; } + private function getMinPhpVersion(): ?PhpVersion + { + if (is_int($this->phpVersion)) { + return null; + } + + if (is_array($this->phpVersion)) { + if ($this->phpVersion['max'] < $this->phpVersion['min']) { + throw new ShouldNotHappenException('Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.'); + } + + return new PhpVersion($this->phpVersion['min']); + } + + return $this->composerPhpVersionFactory->getMinVersion(); + } + + private function getMaxPhpVersion(): ?PhpVersion + { + if (is_int($this->phpVersion)) { + return null; + } + + if (is_array($this->phpVersion)) { + if ($this->phpVersion['max'] < $this->phpVersion['min']) { + throw new ShouldNotHappenException('Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.'); + } + + return new PhpVersion($this->phpVersion['max']); + } + + return $this->composerPhpVersionFactory->getMaxVersion(); + } + public function resolveConstantType(string $constantName, Type $constantType): Type { if ($constantType->isConstantValue()->yes() && in_array($constantName, $this->dynamicConstantNames, true)) { @@ -325,6 +427,14 @@ public function resolveClassConstantType(string $className, string $constantName 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 index bd63830f59..57a3284f76 100644 --- a/src/Analyser/ConstantResolverFactory.php +++ b/src/Analyser/ConstantResolverFactory.php @@ -2,10 +2,13 @@ namespace PHPStan\Analyser; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; +use PHPStan\Php\ComposerPhpVersionFactory; use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; -class ConstantResolverFactory +#[AutowiredService] +final class ConstantResolverFactory { public function __construct( @@ -17,9 +20,13 @@ public function __construct( public function create(): ConstantResolver { + $composerFactory = $this->container->getByType(ComposerPhpVersionFactory::class); + return new ConstantResolver( $this->reflectionProviderProvider, $this->container->getParameter('dynamicConstantNames'), + $this->container->getParameter('phpVersion'), + $composerFactory, ); } diff --git a/src/Analyser/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php index c662e1d9a0..f65a599113 100644 --- a/src/Analyser/DirectInternalScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -7,24 +7,20 @@ use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Parser\Parser; use PHPStan\Php\PhpVersion; -use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\AttributeReflectionFactory; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Properties\PropertyReflectionFinder; -use PHPStan\ShouldNotHappenException; -use function is_a; -class DirectInternalScopeFactory implements InternalScopeFactory +final class DirectInternalScopeFactory implements InternalScopeFactory { /** - * @param class-string $scopeClass + * @param int|array{min: int, max: int}|null $configPhpVersion */ public function __construct( - private string $scopeClass, private ReflectionProvider $reflectionProvider, private InitializerExprTypeResolver $initializerExprTypeResolver, private DynamicReturnTypeExtensionRegistryProvider $dynamicReturnTypeExtensionRegistryProvider, @@ -34,26 +30,19 @@ public function __construct( private PropertyReflectionFinder $propertyReflectionFinder, private Parser $parser, private NodeScopeResolver $nodeScopeResolver, + private RicherScopeGetTypeHelper $richerScopeGetTypeHelper, private PhpVersion $phpVersion, - private bool $explicitMixedInUnknownGenericNew, - private bool $explicitMixedForGlobalVariables, + private AttributeReflectionFactory $attributeReflectionFactory, + private int|array|null $configPhpVersion, private ConstantResolver $constantResolver, ) { } - /** - * @param array $expressionTypes - * @param array $nativeExpressionTypes - * @param array $conditionalExpressions - * @param list $inFunctionCallsStack - * @param array $currentlyAssignedExpressions - * @param array $currentlyAllowedUndefinedExpressions - */ public function create( ScopeContext $context, bool $declareStrictTypes = false, - FunctionReflection|MethodReflection|null $function = null, + PhpFunctionFromParserNodeReflection|null $function = null, ?string $namespace = null, array $expressionTypes = [], array $nativeExpressionTypes = [], @@ -69,12 +58,7 @@ public function create( bool $nativeTypesPromoted = false, ): MutatingScope { - $scopeClass = $this->scopeClass; - if (!is_a($scopeClass, MutatingScope::class, true)) { - throw new ShouldNotHappenException(); - } - - return new $scopeClass( + return new MutatingScope( $this, $this->reflectionProvider, $this->initializerExprTypeResolver, @@ -85,9 +69,12 @@ public function create( $this->propertyReflectionFinder, $this->parser, $this->nodeScopeResolver, + $this->richerScopeGetTypeHelper, $this->constantResolver, $context, $this->phpVersion, + $this->attributeReflectionFactory, + $this->configPhpVersion, $declareStrictTypes, $function, $namespace, @@ -103,8 +90,6 @@ public function create( $afterExtractCall, $parentScope, $nativeTypesPromoted, - $this->explicitMixedInUnknownGenericNew, - $this->explicitMixedForGlobalVariables, ); } diff --git a/src/Analyser/EndStatementResult.php b/src/Analyser/EndStatementResult.php index 32d0809bef..18f97ddc50 100644 --- a/src/Analyser/EndStatementResult.php +++ b/src/Analyser/EndStatementResult.php @@ -4,7 +4,7 @@ use PhpParser\Node\Stmt; -class EndStatementResult +final class EndStatementResult { public function __construct( diff --git a/src/Analyser/EnsuredNonNullabilityResult.php b/src/Analyser/EnsuredNonNullabilityResult.php index 258a16b18b..6a9e539cf1 100644 --- a/src/Analyser/EnsuredNonNullabilityResult.php +++ b/src/Analyser/EnsuredNonNullabilityResult.php @@ -2,7 +2,7 @@ namespace PHPStan\Analyser; -class EnsuredNonNullabilityResult +final class EnsuredNonNullabilityResult { /** diff --git a/src/Analyser/EnsuredNonNullabilityResultExpression.php b/src/Analyser/EnsuredNonNullabilityResultExpression.php index 33f94341e6..59f5eba2ee 100644 --- a/src/Analyser/EnsuredNonNullabilityResultExpression.php +++ b/src/Analyser/EnsuredNonNullabilityResultExpression.php @@ -6,7 +6,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -class EnsuredNonNullabilityResultExpression +final class EnsuredNonNullabilityResultExpression { public function __construct( diff --git a/src/Analyser/Error.php b/src/Analyser/Error.php index f378d912d9..9af5f2b297 100644 --- a/src/Analyser/Error.php +++ b/src/Analyser/Error.php @@ -5,6 +5,7 @@ use Exception; use JsonSerializable; use Nette\Utils\Strings; +use Override; use PhpParser\Node; use PHPStan\ShouldNotHappenException; use ReturnTypeWillChange; @@ -12,8 +13,10 @@ use function is_bool; use function sprintf; -/** @api */ -class Error implements JsonSerializable +/** + * @api + */ +final class Error implements JsonSerializable { public const PATTERN_IDENTIFIER = '[a-zA-Z0-9](?:[a-zA-Z0-9\\.]*[a-zA-Z0-9])?'; @@ -36,6 +39,7 @@ public function __construct( private ?string $nodeType = null, private ?string $identifier = null, private array $metadata = [], + private ?FixedErrorDiff $fixedErrorDiff = null, ) { if ($this->identifier !== null && !self::validateIdentifier($this->identifier)) { @@ -80,6 +84,7 @@ public function changeFilePath(string $newFilePath): self $this->nodeType, $this->identifier, $this->metadata, + $this->fixedErrorDiff, ); } @@ -97,6 +102,7 @@ public function changeTraitFilePath(string $newFilePath): self $this->nodeType, $this->identifier, $this->metadata, + $this->fixedErrorDiff, ); } @@ -141,6 +147,9 @@ public function withoutTip(): self null, $this->nodeLine, $this->nodeType, + $this->identifier, + $this->metadata, + $this->fixedErrorDiff, ); } @@ -160,6 +169,9 @@ public function doNotIgnore(): self $this->tip, $this->nodeLine, $this->nodeType, + $this->identifier, + $this->metadata, + $this->fixedErrorDiff, ); } @@ -181,6 +193,7 @@ public function withIdentifier(string $identifier): self $this->nodeType, $identifier, $this->metadata, + $this->fixedErrorDiff, ); } @@ -205,6 +218,7 @@ public function withMetadata(array $metadata): self $this->nodeType, $this->identifier, $metadata, + $this->fixedErrorDiff, ); } @@ -239,12 +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, @@ -257,6 +287,8 @@ public function jsonSerialize() 'nodeType' => $this->nodeType, 'identifier' => $this->identifier, 'metadata' => $this->metadata, + 'fixedErrorDiffHash' => $fixedErrorDiffHash, + 'fixedErrorDiffDiff' => $fixedErrorDiffDiff, ]; } @@ -265,6 +297,11 @@ public function jsonSerialize() */ 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'], @@ -277,6 +314,7 @@ public static function decode(array $json): self $json['nodeType'] ?? null, $json['identifier'] ?? null, $json['metadata'] ?? [], + $fixedErrorDiff, ); } @@ -297,6 +335,7 @@ public static function __set_state(array $properties): self $properties['nodeType'] ?? null, $properties['identifier'] ?? null, $properties['metadata'] ?? [], + $properties['fixedErrorDiff'] ?? null, ); } diff --git a/src/Analyser/ExpressionContext.php b/src/Analyser/ExpressionContext.php index c2a5a6c78a..c910b0cc3d 100644 --- a/src/Analyser/ExpressionContext.php +++ b/src/Analyser/ExpressionContext.php @@ -4,7 +4,7 @@ use PHPStan\Type\Type; -class ExpressionContext +final class ExpressionContext { private function __construct( diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index cae75b1609..4b586b812b 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -2,7 +2,7 @@ namespace PHPStan\Analyser; -class ExpressionResult +final class ExpressionResult { /** @var (callable(): MutatingScope)|null */ @@ -24,6 +24,7 @@ class ExpressionResult public function __construct( private MutatingScope $scope, private bool $hasYield, + private bool $isAlwaysTerminating, private array $throwPoints, private array $impurePoints, ?callable $truthyScopeCallback = null, @@ -90,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 index 6211e211c8..bb598d8fb1 100644 --- a/src/Analyser/ExpressionTypeHolder.php +++ b/src/Analyser/ExpressionTypeHolder.php @@ -7,7 +7,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ExpressionTypeHolder +final class ExpressionTypeHolder { public function __construct(private Expr $expr, private Type $type, private TrinaryLogic $certainty) @@ -35,15 +35,19 @@ public function equals(self $other): bool public function and(self $other): self { - if ($this->getType()->equals($other->getType())) { - $type = $this->getType(); + if ($this->type->equals($other->type)) { + if ($this->certainty->equals($other->certainty)) { + return $this; + } + + $type = $this->type; } else { - $type = TypeCombinator::union($this->getType(), $other->getType()); + $type = TypeCombinator::union($this->type, $other->type); } return new self( $this->expr, $type, - $this->getCertainty()->and($other->getCertainty()), + $this->certainty->and($other->certainty), ); } diff --git a/src/Analyser/FileAnalyser.php b/src/Analyser/FileAnalyser.php index 13ee1ba1b6..fbdd71d83f 100644 --- a/src/Analyser/FileAnalyser.php +++ b/src/Analyser/FileAnalyser.php @@ -10,7 +10,10 @@ 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\Parser\ParserErrorsException; @@ -31,12 +34,17 @@ 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 { /** @var list */ @@ -48,8 +56,10 @@ class FileAnalyser public function __construct( private ScopeFactory $scopeFactory, private NodeScopeResolver $nodeScopeResolver, + #[AutowiredParameter(ref: '@defaultAnalysisParser')] private Parser $parser, private DependencyResolver $dependencyResolver, + private IgnoreErrorExtensionProvider $ignoreErrorExtensionProvider, private RuleErrorTransformer $ruleErrorTransformer, private LocalIgnoresProcessor $localIgnoresProcessor, ) @@ -74,10 +84,11 @@ public function analyseFile( /** @var list $locallyIgnoredErrors */ $locallyIgnoredErrors = []; - /** @var list $fileCollectedData */ + /** @var CollectorData $fileCollectedData */ $fileCollectedData = []; $fileDependencies = []; + $usedTraitFileDependencies = []; $exportedNodes = []; $linesToIgnore = []; $unmatchedLineIgnores = []; @@ -87,7 +98,7 @@ public function analyseFile( $parserNodes = $this->parser->parseFile($file); $linesToIgnore = $unmatchedLineIgnores = [$file => $this->getLinesToIgnoreFromTokens($parserNodes)]; $temporaryFileErrors = []; - $nodeCallback = function (Node $node, Scope $scope) use (&$fileErrors, &$fileCollectedData, &$fileDependencies, &$exportedNodes, $file, $ruleRegistry, $collectorRegistry, $outerNodeCallback, $analysedFiles, &$linesToIgnore, &$unmatchedLineIgnores, &$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()) { @@ -101,6 +112,15 @@ public function analyseFile( $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); } @@ -115,7 +135,7 @@ public function analyseFile( } $uniquedAnalysedCodeExceptionMessages[$e->getMessage()] = true; - $fileErrors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $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), @@ -123,7 +143,7 @@ public function analyseFile( ]); continue; } catch (IdentifierNotFound $e) { - $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getStartLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols')) + $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), @@ -141,7 +161,17 @@ public function analyseFile( } foreach ($ruleErrors as $ruleError) { - $temporaryFileErrors[] = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine()); + $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; + } + } + } + + $temporaryFileErrors[] = $error; } } @@ -154,7 +184,7 @@ public function analyseFile( } $uniquedAnalysedCodeExceptionMessages[$e->getMessage()] = true; - $fileErrors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $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), @@ -162,7 +192,7 @@ public function analyseFile( ]); continue; } catch (IdentifierNotFound $e) { - $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getStartLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols')) + $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), @@ -183,11 +213,7 @@ public function analyseFile( continue; } - $fileCollectedData[] = new CollectedData( - $collectedData, - $scope->getFile(), - get_class($collector), - ); + $fileCollectedData[$scope->getFile()][get_class($collector)][] = $collectedData; } try { @@ -205,6 +231,15 @@ public function analyseFile( } 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)); @@ -235,35 +270,35 @@ public function analyseFile( $fileErrors[] = (new Error($error->getMessage(), $e->getParsedFile() ?? $file, $error->getLine() !== -1 ? $error->getStartLine() : null, $e))->withIdentifier('phpstan.parse'); } } catch (AnalysedCodeException $e) { - $fileErrors[] = (new Error($e->getMessage(), $file, null, $e, null, null, $e->getTip())) + $fileErrors[] = (new Error($e->getMessage(), $file, 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')) + $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, null, $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))->withIdentifier('phpstan.path'); + $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))->withIdentifier('phpstan.path'); + $fileErrors[] = (new Error(sprintf('File %s does not exist.', $file), $file, canBeIgnored: false))->withIdentifier('phpstan.path'); } - $this->restoreCollectErrorsHandler(); - foreach ($linesToIgnore as $fileKey => $lines) { if (count($lines) > 0) { continue; @@ -287,6 +322,7 @@ public function analyseFile( $locallyIgnoredErrors, $fileCollectedData, array_values(array_unique($fileDependencies)), + array_values(array_unique($usedTraitFileDependencies)), $exportedNodes, $linesToIgnore, $unmatchedLineIgnores, @@ -322,7 +358,7 @@ private function collectErrors(array $analysedFiles): void $errorMessage = sprintf('%s: %s', $this->getErrorLabel($errno), $errstr); - $this->allPhpErrors[] = (new Error($errorMessage, $errfile, $errline, true))->withIdentifier('phpstan.php'); + $this->allPhpErrors[] = (new Error($errorMessage, $errfile, $errline, false))->withIdentifier('phpstan.php'); if ($errno === E_DEPRECATED) { return true; @@ -332,7 +368,7 @@ private function collectErrors(array $analysedFiles): void return true; } - $this->filteredPhpErrors[] = (new Error($errorMessage, $errfile, $errline, true))->withIdentifier('phpstan.php'); + $this->filteredPhpErrors[] = (new Error($errorMessage, $errfile, $errline, $errno === E_USER_DEPRECATED))->withIdentifier('phpstan.php'); return true; }); @@ -354,12 +390,16 @@ private function getErrorLabel(int $errno): string 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)'; } diff --git a/src/Analyser/FileAnalyserResult.php b/src/Analyser/FileAnalyserResult.php index 23e34a9278..0d3140f6f8 100644 --- a/src/Analyser/FileAnalyserResult.php +++ b/src/Analyser/FileAnalyserResult.php @@ -7,8 +7,9 @@ /** * @phpstan-type LinesToIgnore = array|null>> + * @phpstan-import-type CollectorData from CollectedData */ -class FileAnalyserResult +final class FileAnalyserResult { /** @@ -16,8 +17,9 @@ class FileAnalyserResult * @param list $filteredPhpErrors * @param list $allPhpErrors * @param list $locallyIgnoredErrors - * @param list $collectedData + * @param CollectorData $collectedData * @param list $dependencies + * @param list $usedTraitDependencies * @param list $exportedNodes * @param LinesToIgnore $linesToIgnore * @param LinesToIgnore $unmatchedLineIgnores @@ -29,6 +31,7 @@ public function __construct( private array $locallyIgnoredErrors, private array $collectedData, private array $dependencies, + private array $usedTraitDependencies, private array $exportedNodes, private array $linesToIgnore, private array $unmatchedLineIgnores, @@ -69,7 +72,7 @@ public function getLocallyIgnoredErrors(): array } /** - * @return list + * @return CollectorData */ public function getCollectedData(): array { @@ -84,6 +87,14 @@ public function getDependencies(): array return $this->dependencies; } + /** + * @return list + */ + public function getUsedTraitDependencies(): array + { + return $this->usedTraitDependencies; + } + /** * @return list */ diff --git a/src/Analyser/FinalizerResult.php b/src/Analyser/FinalizerResult.php index dfc7761743..c196b25d99 100644 --- a/src/Analyser/FinalizerResult.php +++ b/src/Analyser/FinalizerResult.php @@ -2,7 +2,7 @@ namespace PHPStan\Analyser; -class FinalizerResult +final class FinalizerResult { /** 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 @@ +regexp === null) { - $this->regexp = $this->generateRegexp(); - } + $this->regexp ??= $this->generateRegexp(); $matches = Strings::matchAll($input, $this->regexp, PREG_SET_ORDER); diff --git a/src/Analyser/Ignore/IgnoreParseException.php b/src/Analyser/Ignore/IgnoreParseException.php index fc8eec134c..c355cb5ad2 100644 --- a/src/Analyser/Ignore/IgnoreParseException.php +++ b/src/Analyser/Ignore/IgnoreParseException.php @@ -4,7 +4,7 @@ use Exception; -class IgnoreParseException extends Exception +final class IgnoreParseException extends Exception { public function __construct(string $message, private int $phpDocLine) diff --git a/src/Analyser/Ignore/IgnoredError.php b/src/Analyser/Ignore/IgnoredError.php index ecbe6e1059..8f44d3cb28 100644 --- a/src/Analyser/Ignore/IgnoredError.php +++ b/src/Analyser/Ignore/IgnoredError.php @@ -4,7 +4,6 @@ use Nette\Utils\Strings; use PHPStan\Analyser\Error; -use PHPStan\DependencyInjection\BleedingEdgeToggle; use PHPStan\File\FileExcluder; use PHPStan\File\FileHelper; use PHPStan\ShouldNotHappenException; @@ -15,7 +14,7 @@ use function sprintf; use function str_replace; -class IgnoredError +final class IgnoredError { /** @@ -38,6 +37,13 @@ public static function stringifyPattern($ignoredError): string } else { $message = sprintf('%s (%s)', $message, $ignoredError['identifier']); } + } elseif (isset($ignoredError['identifiers'])) { + $identifierList = implode(', ', $ignoredError['identifiers']); + if ($message === '') { + $message = $identifierList; + } else { + $message = sprintf('%s (%s)', $message, $identifierList); + } } if ($message === '') { @@ -86,7 +92,7 @@ public static function shouldIgnore( } if ($path !== null) { - $fileExcluder = new FileExcluder($fileHelper, [$path], BleedingEdgeToggle::isBleedingEdge()); + $fileExcluder = new FileExcluder($fileHelper, [$path]); $isExcluded = $fileExcluder->isExcludedFromAnalysing($error->getFilePath()); if (!$isExcluded && $error->getTraitFilePath() !== null) { return $fileExcluder->isExcludedFromAnalysing($error->getTraitFilePath()); diff --git a/src/Analyser/Ignore/IgnoredErrorHelper.php b/src/Analyser/Ignore/IgnoredErrorHelper.php index bff07c1085..29fb13d818 100644 --- a/src/Analyser/Ignore/IgnoredErrorHelper.php +++ b/src/Analyser/Ignore/IgnoredErrorHelper.php @@ -4,6 +4,8 @@ use Nette\Utils\Json; use Nette\Utils\JsonException; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\File\FileHelper; use PHPStan\ShouldNotHappenException; use function array_key_exists; @@ -12,7 +14,8 @@ use function is_file; use function sprintf; -class IgnoredErrorHelper +#[AutowiredService] +final class IgnoredErrorHelper { /** @@ -20,7 +23,9 @@ class IgnoredErrorHelper */ public function __construct( private FileHelper $fileHelper, + #[AutowiredParameter] private array $ignoreErrors, + #[AutowiredParameter] private bool $reportUnmatchedIgnoredErrors, ) { @@ -35,7 +40,7 @@ public function initialize(): IgnoredErrorHelperResult $expandedIgnoreErrors = []; foreach ($this->ignoreErrors as $ignoreError) { if (is_array($ignoreError)) { - if (!isset($ignoreError['message']) && !isset($ignoreError['messages']) && !isset($ignoreError['identifier'])) { + if (!isset($ignoreError['message']) && !isset($ignoreError['messages']) && !isset($ignoreError['identifier']) && !isset($ignoreError['identifiers'])) { $errors[] = sprintf( 'Ignored error %s is missing a message or an identifier.', Json::encode($ignoreError), @@ -49,6 +54,13 @@ public function initialize(): IgnoredErrorHelperResult $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; } diff --git a/src/Analyser/Ignore/IgnoredErrorHelperProcessedResult.php b/src/Analyser/Ignore/IgnoredErrorHelperProcessedResult.php index 4ffacf46d4..67bcfa176c 100644 --- a/src/Analyser/Ignore/IgnoredErrorHelperProcessedResult.php +++ b/src/Analyser/Ignore/IgnoredErrorHelperProcessedResult.php @@ -4,7 +4,7 @@ use PHPStan\Analyser\Error; -class IgnoredErrorHelperProcessedResult +final class IgnoredErrorHelperProcessedResult { /** diff --git a/src/Analyser/Ignore/IgnoredErrorHelperResult.php b/src/Analyser/Ignore/IgnoredErrorHelperResult.php index 95d4273ef8..3358c9e63a 100644 --- a/src/Analyser/Ignore/IgnoredErrorHelperResult.php +++ b/src/Analyser/Ignore/IgnoredErrorHelperResult.php @@ -13,7 +13,7 @@ use function is_string; use function sprintf; -class IgnoredErrorHelperResult +final class IgnoredErrorHelperResult { /** @@ -224,8 +224,7 @@ public function process( IgnoredError::stringifyPattern($unmatchedIgnoredError), ), $unmatchedIgnoredError['realPath'], - null, - false, + canBeIgnored: false, ))->withIdentifier('ignore.unmatched'); } elseif (!$onlyFiles) { $stringErrors[] = sprintf( 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/ImpurePoint.php b/src/Analyser/ImpurePoint.php index e87e8866b0..dc9e9a6091 100644 --- a/src/Analyser/ImpurePoint.php +++ b/src/Analyser/ImpurePoint.php @@ -6,10 +6,10 @@ use PHPStan\Node\VirtualNode; /** - * @phpstan-type ImpurePointIdentifier = 'echo'|'die'|'exit'|'propertyAssign'|'propertyAssignByRef'|'propertyUnset'|'methodCall'|'new'|'functionCall'|'include'|'require'|'print'|'eval'|'superglobal'|'yield'|'yieldFrom'|'static'|'global'|'betweenPhpTags' + * @phpstan-type ImpurePointIdentifier = 'echo'|'die'|'exit'|'propertyAssign'|'propertyAssignByRef'|'propertyUnset'|'methodCall'|'new'|'functionCall'|'include'|'require'|'print'|'eval'|'superglobal'|'yield'|'yieldFrom'|'static'|'global'|'betweenPhpTags'|'staticPropertyAccess' * @api */ -class ImpurePoint +final class ImpurePoint { /** diff --git a/src/Analyser/InternalError.php b/src/Analyser/InternalError.php index 9a3ddd2820..6aaad7240d 100644 --- a/src/Analyser/InternalError.php +++ b/src/Analyser/InternalError.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser; use JsonSerializable; +use Override; use ReturnTypeWillChange; use Throwable; use function array_map; @@ -12,7 +13,7 @@ * @api * @phpstan-type Trace = list */ -class InternalError implements JsonSerializable +final class InternalError implements JsonSerializable { public const STACK_TRACE_METADATA_KEY = 'stackTrace'; @@ -90,6 +91,7 @@ public static function decode(array $json): self * @return mixed */ #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ diff --git a/src/Analyser/InternalScopeFactory.php b/src/Analyser/InternalScopeFactory.php index fdacebc9ec..8d8daa714f 100644 --- a/src/Analyser/InternalScopeFactory.php +++ b/src/Analyser/InternalScopeFactory.php @@ -6,6 +6,7 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; interface InternalScopeFactory { @@ -17,12 +18,12 @@ interface InternalScopeFactory * @param list $inClosureBindScopeClasses * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions - * @param list $inFunctionCallsStack + * @param list $inFunctionCallsStack */ public function create( ScopeContext $context, bool $declareStrictTypes = false, - FunctionReflection|MethodReflection|null $function = null, + PhpFunctionFromParserNodeReflection|null $function = null, ?string $namespace = null, array $expressionTypes = [], array $nativeExpressionTypes = [], diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index 028600646c..91d8c09a98 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -2,52 +2,37 @@ namespace PHPStan\Analyser; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider; use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Php\PhpVersion; -use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\AttributeReflectionFactory; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Properties\PropertyReflectionFinder; -use PHPStan\ShouldNotHappenException; -use function is_a; -class LazyInternalScopeFactory implements InternalScopeFactory +#[AutowiredService(as: InternalScopeFactory::class)] +final class LazyInternalScopeFactory implements InternalScopeFactory { - private bool $explicitMixedInUnknownGenericNew; + /** @var int|array{min: int, max: int}|null */ + private int|array|null $phpVersion; - private bool $explicitMixedForGlobalVariables; - - /** - * @param class-string $scopeClass - */ public function __construct( - private string $scopeClass, private Container $container, ) { - $this->explicitMixedInUnknownGenericNew = $this->container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew']; - $this->explicitMixedForGlobalVariables = $this->container->getParameter('featureToggles')['explicitMixedForGlobalVariables']; + $this->phpVersion = $this->container->getParameter('phpVersion'); } - /** - * @param array $expressionTypes - * @param array $nativeExpressionTypes - * @param array $conditionalExpressions - * @param array $currentlyAssignedExpressions - * @param array $currentlyAllowedUndefinedExpressions - * @param list $inFunctionCallsStack - */ public function create( ScopeContext $context, bool $declareStrictTypes = false, - FunctionReflection|MethodReflection|null $function = null, + PhpFunctionFromParserNodeReflection|null $function = null, ?string $namespace = null, array $expressionTypes = [], array $nativeExpressionTypes = [], @@ -63,12 +48,7 @@ public function create( bool $nativeTypesPromoted = false, ): MutatingScope { - $scopeClass = $this->scopeClass; - if (!is_a($scopeClass, MutatingScope::class, true)) { - throw new ShouldNotHappenException(); - } - - return new $scopeClass( + return new MutatingScope( $this, $this->container->getByType(ReflectionProvider::class), $this->container->getByType(InitializerExprTypeResolver::class), @@ -79,9 +59,12 @@ public function create( $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, @@ -97,8 +80,6 @@ public function create( $afterExtractCall, $parentScope, $nativeTypesPromoted, - $this->explicitMixedInUnknownGenericNew, - $this->explicitMixedForGlobalVariables, ); } diff --git a/src/Analyser/LocalIgnoresProcessor.php b/src/Analyser/LocalIgnoresProcessor.php index 04368960b0..192b909251 100644 --- a/src/Analyser/LocalIgnoresProcessor.php +++ b/src/Analyser/LocalIgnoresProcessor.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser; +use PHPStan\DependencyInjection\AutowiredService; use function array_key_exists; use function array_values; use function count; @@ -10,7 +11,8 @@ /** * @phpstan-import-type LinesToIgnore from FileAnalyserResult */ -class LocalIgnoresProcessor +#[AutowiredService] +final class LocalIgnoresProcessor { /** diff --git a/src/Analyser/LocalIgnoresProcessorResult.php b/src/Analyser/LocalIgnoresProcessorResult.php index 8c7a36287f..1d44c98f1f 100644 --- a/src/Analyser/LocalIgnoresProcessorResult.php +++ b/src/Analyser/LocalIgnoresProcessorResult.php @@ -5,7 +5,7 @@ /** * @phpstan-import-type LinesToIgnore from FileAnalyserResult */ -class LocalIgnoresProcessorResult +final class LocalIgnoresProcessorResult { /** diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 6632716bd7..57d047fbf8 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -7,6 +7,7 @@ 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; @@ -21,12 +22,14 @@ 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; @@ -49,16 +52,22 @@ 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; @@ -67,7 +76,6 @@ use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\Php\DummyParameter; use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; @@ -101,6 +109,7 @@ 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; @@ -144,6 +153,7 @@ 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; @@ -158,7 +168,7 @@ use const PHP_INT_MAX; use const PHP_INT_MIN; -class MutatingScope implements Scope +final class MutatingScope implements Scope { private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4; @@ -184,6 +194,7 @@ class MutatingScope implements Scope private static int $resolveClosureTypeDepth = 0; /** + * @param int|array{min: int, max: int}|null $configPhpVersion * @param array $expressionTypes * @param array $conditionalExpressions * @param list $inClosureBindScopeClasses @@ -203,11 +214,14 @@ public function __construct( 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 FunctionReflection|ExtendedMethodReflection|null $function = null, + private PhpFunctionFromParserNodeReflection|null $function = null, ?string $namespace = null, private array $expressionTypes = [], private array $nativeExpressionTypes = [], @@ -221,8 +235,6 @@ public function __construct( private bool $afterExtractCall = false, private ?Scope $parentScope = null, private bool $nativeTypesPromoted = false, - private bool $explicitMixedInUnknownGenericNew = false, - private bool $explicitMixedForGlobalVariables = false, ) { if ($namespace === '') { @@ -283,6 +295,77 @@ public function enterDeclareStrictTypes(): self ); } + /** + * @param array $currentExpressionTypes + * @return array + */ + private function rememberConstructorExpressions(array $currentExpressionTypes): array + { + $expressionTypes = []; + foreach ($currentExpressionTypes as $exprString => $expressionTypeHolder) { + $expr = $expressionTypeHolder->getExpr(); + if ($expr instanceof FuncCall) { + if ( + !$expr->name instanceof Name + || !in_array($expr->name->name, ['class_exists', 'function_exists'], true) + ) { + continue; + } + } elseif ($expr instanceof PropertyFetch) { + if ( + !$expr->name instanceof Node\Identifier + || !$expr->var instanceof Variable + || $expr->var->name !== 'this' + || !$this->phpVersion->supportsReadOnlyProperties() + ) { + continue; + } + + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this); + if ($propertyReflection === null) { + continue; + } + + $nativePropertyReflection = $propertyReflection->getNativeReflection(); + if ($nativePropertyReflection === null || !$nativePropertyReflection->isReadOnly()) { + continue; + } + } elseif (!$expr instanceof ConstFetch && !$expr instanceof PropertyInitializationExpr) { + continue; + } + + $expressionTypes[$exprString] = $expressionTypeHolder; + } + + if (array_key_exists('$this', $currentExpressionTypes)) { + $expressionTypes['$this'] = $currentExpressionTypes['$this']; + } + + return $expressionTypes; + } + + public function rememberConstructorScope(): self + { + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + null, + $this->getNamespace(), + $this->rememberConstructorExpressions($this->expressionTypes), + $this->rememberConstructorExpressions($this->nativeExpressionTypes), + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + [], + [], + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + } + /** @api */ public function isInClass(): bool { @@ -309,9 +392,8 @@ public function getTraitReflection(): ?ClassReflection /** * @api - * @return FunctionReflection|ExtendedMethodReflection|null */ - public function getFunction() + public function getFunction(): ?PhpFunctionFromParserNodeReflection { return $this->function; } @@ -527,26 +609,26 @@ public function getVariableType(string $variableName): Type return IntegerRangeType::fromInterval(1, null); } if ($variableName === 'argv') { - return AccessoryArrayListType::intersectWith(TypeCombinator::intersect( + return TypeCombinator::intersect( new ArrayType(new IntegerType(), new StringType()), new NonEmptyArrayType(), - )); + new AccessoryArrayListType(), + ); } if ($this->canAnyVariableExist()) { return new MixedType(); } } - if ($this->isGlobalVariable($variableName)) { - return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType($this->explicitMixedForGlobalVariables)); - } - if ($this->hasVariableType($variableName)->no()) { throw new UndefinedVariableException($this, $variableName); } $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(); } @@ -555,7 +637,7 @@ public function getVariableType(string $variableName): Type /** * @api - * @return array + * @return list */ public function getDefinedVariables(): array { @@ -574,6 +656,27 @@ public function getDefinedVariables(): array return $variables; } + /** + * @api + * @return list + */ + public function getMaybeDefinedVariables(): array + { + $variables = []; + foreach ($this->expressionTypes as $exprString => $holder) { + if (!$holder->getExpr() instanceof Variable) { + continue; + } + if (!$holder->getCertainty()->maybe()) { + continue; + } + + $variables[] = substr($exprString, 1); + } + + return $variables; + } + private function isGlobalVariable(string $variableName): bool { return in_array($variableName, self::SUPERGLOBAL_VARIABLES, true); @@ -587,12 +690,7 @@ public function hasConstant(Name $name): bool return $this->fileHasCompilerHaltStatementCalls(); } - if (!$name->isFullyQualified() && $this->getNamespace() !== null) { - if ($this->hasExpressionType(new ConstFetch(new FullyQualified([$this->getNamespace(), $name->toString()])))->yes()) { - return true; - } - } - if ($this->hasExpressionType(new ConstFetch(new FullyQualified($name->toString())))->yes()) { + if ($this->getGlobalConstantType($name) !== null) { return true; } @@ -688,15 +786,16 @@ private function getNodeKey(Expr $node): string { $key = $this->exprPrinter->printExpr($node); + $attributes = $node->getAttributes(); if ( $node instanceof Node\FunctionLike - && $node->hasAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME) - && $node->hasAttribute('startFilePos') + && (($attributes[ArrayMapArgVisitor::ATTRIBUTE_NAME] ?? null) !== null) + && (($attributes['startFilePos'] ?? null) !== null) ) { - $key .= '/*' . $node->getAttribute('startFilePos') . '*/'; + $key .= '/*' . $attributes['startFilePos'] . '*/'; } - if ($node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME) === true) { + if (($attributes[self::KEEP_VOID_ATTRIBUTE_NAME] ?? null) === true) { $key .= '/*' . self::KEEP_VOID_ATTRIBUTE_NAME . '*/'; } @@ -745,23 +844,23 @@ private function resolveType(string $exprString, Expr $node): Type } if ($node instanceof AlwaysRememberedExpr) { - return $node->getExprType(); + 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) { @@ -778,7 +877,7 @@ private function resolveType(string $exprString, Expr $node): Type $leftType = $this->getType($node->left); $rightType = $this->getType($node->right); - return $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType); + return $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType)->type; } if ($node instanceof Expr\BinaryOp\NotEqual) { @@ -903,46 +1002,11 @@ private function resolveType(string $exprString, Expr $node): Type } if ($node instanceof Expr\BinaryOp\Identical) { - if ( - $node->left instanceof Variable - && is_string($node->left->name) - && $node->right instanceof Variable - && is_string($node->right->name) - && $node->left->name === $node->right->name - ) { - return new ConstantBooleanType(true); - } - - $leftType = $this->getType($node->left); - $rightType = $this->getType($node->right); - - if ( - ( - $node->left instanceof Node\Expr\PropertyFetch - || $node->left instanceof Node\Expr\StaticPropertyFetch - ) - && $rightType->isNull()->yes() - && !$this->hasPropertyNativeType($node->left) - ) { - return new BooleanType(); - } - - if ( - ( - $node->right instanceof Node\Expr\PropertyFetch - || $node->right instanceof Node\Expr\StaticPropertyFetch - ) - && $leftType->isNull()->yes() - && !$this->hasPropertyNativeType($node->right) - ) { - return new BooleanType(); - } - - return $this->initializerExprTypeResolver->resolveIdenticalType($leftType, $rightType); + return $this->richerScopeGetTypeHelper->getIdenticalResult($this, $node)->type; } if ($node instanceof Expr\BinaryOp\NotIdentical) { - return $this->getType(new Expr\BooleanNot(new BinaryOp\Identical($node->left, $node->right))); + return $this->richerScopeGetTypeHelper->getNotIdenticalResult($this, $node)->type; } if ($node instanceof Expr\Instanceof_) { @@ -1124,31 +1188,28 @@ private function resolveType(string $exprString, Expr $node): Type return $this->getType($node->expr); } - if ($node instanceof LNumber) { + 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\Encapsed) { + } elseif ($node instanceof Node\Scalar\InterpolatedString) { $resultType = null; - foreach ($node->parts as $part) { - $partType = $part instanceof EncapsedStringPart - ? new ConstantStringType($part->value) - : $this->getType($part)->toString(); + if ($part instanceof InterpolatedStringPart) { + $partType = new ConstantStringType($part->value); + } else { + $partType = $this->getType($part)->toString(); + } if ($resultType === null) { $resultType = $partType; - continue; } $resultType = $this->initializerExprTypeResolver->resolveConcatType($resultType, $partType); - if (count($resultType->getConstantStrings()) === 0) { - return $resultType; - } } return $resultType ?? new ConstantStringType(''); - } elseif ($node instanceof DNumber) { + } 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) { @@ -1201,7 +1262,7 @@ private function resolveType(string $exprString, Expr $node): Type return new ObjectType(Closure::class); } - $classType = $this->resolveTypeByName($node->class); + $classType = $this->resolveTypeByNameWithLateStaticBinding($node->class, $node->name); $methodName = $node->name->toString(); if (!$classType->hasMethod($methodName)->yes()) { return new ObjectType(Closure::class); @@ -1355,11 +1416,11 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu TemplateTypeMap::createEmpty(), TemplateTypeMap::createEmpty(), TemplateTypeVarianceMap::createEmpty(), - [], - $cachedClosureData['throwPoints'], - $cachedClosureData['impurePoints'], - $cachedClosureData['invalidateExpressions'], - $cachedClosureData['usedVariables'], + throwPoints: $cachedClosureData['throwPoints'], + impurePoints: $cachedClosureData['impurePoints'], + invalidateExpressions: $cachedClosureData['invalidateExpressions'], + usedVariables: $cachedClosureData['usedVariables'], + acceptsNamedArguments: TrinaryLogic::createYes(), ); } if (self::$resolveClosureTypeDepth >= 2) { @@ -1569,11 +1630,11 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu TemplateTypeMap::createEmpty(), TemplateTypeMap::createEmpty(), TemplateTypeVarianceMap::createEmpty(), - [], - $throwPointsForClosureType, - $impurePointsForClosureType, - $invalidateExpressions, - $usedVariables, + throwPoints: $throwPointsForClosureType, + impurePoints: $impurePointsForClosureType, + invalidateExpressions: $invalidateExpressions, + usedVariables: $usedVariables, + acceptsNamedArguments: TrinaryLogic::createYes(), ); } elseif ($node instanceof New_) { if ($node->class instanceof Name) { @@ -1704,17 +1765,17 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu } 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(); + $returnType = $functionReflection->getReturnType(); $generatorSendType = $returnType->getTemplateType(Generator::class, 'TSend'); if ($generatorSendType instanceof ErrorType) { return new MixedType(); @@ -1834,7 +1895,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu } else { $items = []; foreach ($arm->conds as $filteringExpr) { - $items[] = new Expr\ArrayItem($filteringExpr); + $items[] = new Node\ArrayItem($filteringExpr); } $filteringExpr = new FuncCall( new Name\FullyQualified('in_array'), @@ -2008,12 +2069,33 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu ); } - if ($node instanceof Variable && is_string($node->name)) { - if ($this->hasVariableType($node->name)->no()) { - return new ErrorType(); + if ($node instanceof Variable) { + if (is_string($node->name)) { + if ($this->hasVariableType($node->name)->no()) { + return new ErrorType(); + } + + return $this->getVariableType($node->name); } - return $this->getVariableType($node->name); + $nameType = $this->getType($node->name); + if (count($nameType->getConstantStrings()) > 0) { + $types = []; + foreach ($nameType->getConstantStrings() as $constantString) { + $variableScope = $this + ->filterByTruthyValue( + new BinaryOp\Identical($node->name, new String_($constantString->getValue())), + ); + if ($variableScope->hasVariableType($constantString->getValue())->no()) { + $types[] = new ErrorType(); + continue; + } + + $types[] = $variableScope->getVariableType($constantString->getValue()); + } + + return TypeCombinator::union(...$types); + } } if ($node instanceof Expr\ArrayDimFetch && $node->dim !== null) { @@ -2027,36 +2109,41 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu ); } - if ($node instanceof MethodCall && $node->name instanceof Node\Identifier) { - if ($this->nativeTypesPromoted) { - $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) { - return new ErrorType(); + $returnType = new ErrorType(); + } else { + $returnType = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType(); } - return ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType(); - }; - - return $this->getNullsafeShortCircuitingType($node->var, $typeCallback()); - } + return $this->getNullsafeShortCircuitingType($node->var, $returnType); + } - $typeCallback = function () use ($node): Type { $returnType = $this->methodCallReturnType( $this->getType($node->var), $node->name->name, $node, ); if ($returnType === null) { - return new ErrorType(); + $returnType = new ErrorType(); } - return $returnType; - }; + return $this->getNullsafeShortCircuitingType($node->var, $returnType); + } - return $this->getNullsafeShortCircuitingType($node->var, $typeCallback()); + $nameType = $this->getType($node->name); + if (count($nameType->getConstantStrings()) > 0) { + return TypeCombinator::union( + ...array_map(fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $this + ->filterByTruthyValue(new BinaryOp\Identical($node->name, new String_($constantString->getValue()))) + ->getType(new MethodCall($node->var, new Identifier($constantString->getValue()), $node->args)), $nameType->getConstantStrings()), + ); + } } if ($node instanceof Expr\NullsafeMethodCall) { @@ -2075,11 +2162,11 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu ); } - if ($node instanceof Expr\StaticCall && $node->name instanceof Node\Identifier) { - if ($this->nativeTypesPromoted) { - $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->resolveTypeByName($node->class); + $staticMethodCalledOnType = $this->resolveTypeByNameWithLateStaticBinding($node->class, $node->name); } else { $staticMethodCalledOnType = $this->getNativeType($node->class); } @@ -2088,73 +2175,89 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu $node->name->name, ); if ($methodReflection === null) { - return new ErrorType(); + $callType = new ErrorType(); + } else { + $callType = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType(); } - return ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType(); - }; + if ($node->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($node->class, $callType); + } - $callType = $typeCallback(); - if ($node->class instanceof Expr) { - return $this->getNullsafeShortCircuitingType($node->class, $callType); + return $callType; } - return $callType; - } - - $typeCallback = function () use ($node): Type { if ($node->class instanceof Name) { - $staticMethodCalledOnType = $this->resolveTypeByName($node->class); + $staticMethodCalledOnType = $this->resolveTypeByNameWithLateStaticBinding($node->class, $node->name); } else { $staticMethodCalledOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType(); } - $returnType = $this->methodCallReturnType( + $callType = $this->methodCallReturnType( $staticMethodCalledOnType, $node->name->toString(), $node, ); - if ($returnType === null) { - return new ErrorType(); + if ($callType === null) { + $callType = new ErrorType(); } - return $returnType; - }; - $callType = $typeCallback(); - if ($node->class instanceof Expr) { - return $this->getNullsafeShortCircuitingType($node->class, $callType); + 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) { - if ($this->nativeTypesPromoted) { - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node, $this); - if ($propertyReflection === null) { - return new ErrorType(); - } - $nativeType = $propertyReflection->getNativeType(); - if ($nativeType === null) { - return new ErrorType(); - } + 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(); + } - return $this->getNullsafeShortCircuitingType($node->var, $nativeType); - } + if (!$propertyReflection->hasNativeType()) { + return new MixedType(); + } + + $nativeType = $propertyReflection->getNativeType(); + + return $this->getNullsafeShortCircuitingType($node->var, $nativeType); + } - $typeCallback = function () use ($node): Type { $returnType = $this->propertyFetchType( $this->getType($node->var), $node->name->name, $node, ); if ($returnType === null) { - return new ErrorType(); + $returnType = new ErrorType(); } - return $returnType; - }; - return $this->getNullsafeShortCircuitingType($node->var, $typeCallback()); + return $this->getNullsafeShortCircuitingType($node->var, $returnType); + } + + $nameType = $this->getType($node->name); + if (count($nameType->getConstantStrings()) > 0) { + return TypeCombinator::union( + ...array_map(fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $this + ->filterByTruthyValue(new BinaryOp\Identical($node->name, new String_($constantString->getValue()))) + ->getType( + new PropertyFetch($node->var, new Identifier($constantString->getValue())), + ), $nameType->getConstantStrings()), + ); + } } if ($node instanceof Expr\NullsafePropertyFetch) { @@ -2173,51 +2276,56 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu ); } - if ( - $node instanceof Expr\StaticPropertyFetch - && $node->name instanceof Node\VarLikeIdentifier - ) { - if ($this->nativeTypesPromoted) { - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node, $this); - if ($propertyReflection === null) { - return new ErrorType(); - } - $nativeType = $propertyReflection->getNativeType(); - if ($nativeType === null) { - return new ErrorType(); - } + 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(); + } - if ($node->class instanceof Expr) { - return $this->getNullsafeShortCircuitingType($node->class, $nativeType); - } + $nativeType = $propertyReflection->getNativeType(); - return $nativeType; - } + if ($node->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($node->class, $nativeType); + } + + return $nativeType; + } - $typeCallback = function () use ($node): Type { if ($node->class instanceof Name) { $staticPropertyFetchedOnType = $this->resolveTypeByName($node->class); } else { $staticPropertyFetchedOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType(); } - $returnType = $this->propertyFetchType( + $fetchType = $this->propertyFetchType( $staticPropertyFetchedOnType, $node->name->toString(), $node, ); - if ($returnType === null) { - return new ErrorType(); + if ($fetchType === null) { + $fetchType = new ErrorType(); } - return $returnType; - }; - $fetchType = $typeCallback(); - if ($node->class instanceof Expr) { - return $this->getNullsafeShortCircuitingType($node->class, $fetchType); + if ($node->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($node->class, $fetchType); + } + + return $fetchType; } - return $fetchType; + $nameType = $this->getType($node->name); + if (count($nameType->getConstantStrings()) > 0) { + return TypeCombinator::union( + ...array_map(fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $this + ->filterByTruthyValue(new BinaryOp\Identical($node->name, new String_($constantString->getValue()))) + ->getType(new Expr\StaticPropertyFetch($node->class, new Node\VarLikeIdentifier($constantString->getValue()))), $nameType->getConstantStrings()), + ); + } } if ($node instanceof FuncCall) { @@ -2227,12 +2335,35 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu return new ErrorType(); } - return ParametersAcceptorSelector::selectFromArgs( + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $this, $node->getArgs(), $calledOnType->getCallableParametersAcceptors($this), null, - )->getReturnType(); + ); + + $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)) { @@ -2261,19 +2392,9 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu ); $normalizedNode = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); if ($normalizedNode !== null) { - foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicFunctionReturnTypeExtensions() as $dynamicFunctionReturnTypeExtension) { - if (!$dynamicFunctionReturnTypeExtension->isFunctionSupported($functionReflection)) { - continue; - } - - $resolvedType = $dynamicFunctionReturnTypeExtension->getTypeFromFunctionCall( - $functionReflection, - $normalizedNode, - $this, - ); - if ($resolvedType !== null) { - return $resolvedType; - } + $resolvedType = $this->getDynamicFunctionReturnType($parametersAcceptor, $normalizedNode, $functionReflection); + if ($resolvedType !== null) { + return $resolvedType; } } @@ -2283,6 +2404,29 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu 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) { @@ -2414,8 +2558,7 @@ public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = n return null; } - $nativeType = $propertyReflection->getNativeType(); - if (!$nativeType instanceof MixedType) { + if ($propertyReflection->hasNativeType() && !$propertyReflection->isVirtual()->yes()) { if (!$this->hasExpressionType($expr)->yes()) { if ($expr instanceof Node\Expr\PropertyFetch) { return $this->issetCheckUndefined($expr->var); @@ -2504,7 +2647,7 @@ private function createFirstClassCallable( foreach ($variants as $variant) { $returnType = $variant->getReturnType(); - if ($variant instanceof ParametersAcceptorWithPhpDocs) { + if ($variant instanceof ExtendedParametersAcceptor) { $returnType = $this->nativeTypesPromoted ? $variant->getNativeReturnType() : $returnType; } @@ -2516,15 +2659,18 @@ private function createFirstClassCallable( $templateTags[$templateType->getName()] = new TemplateTag( $templateType->getName(), $templateType->getBound(), + $templateType->getDefault(), $templateType->getVariance(), ); } $throwPoints = []; $impurePoints = []; + $acceptsNamedArguments = TrinaryLogic::createYes(); if ($variant instanceof CallableParametersAcceptor) { $throwPoints = $variant->getThrowPoints(); $impurePoints = $variant->getImpurePoints(); + $acceptsNamedArguments = $variant->acceptsNamedArguments(); } elseif ($function !== null) { $returnTypeForThrow = $variant->getReturnType(); $throwType = $function->getThrowType(); @@ -2548,6 +2694,8 @@ private function createFirstClassCallable( if ($impurePoint !== null) { $impurePoints[] = $impurePoint; } + + $acceptsNamedArguments = $function->acceptsNamedArguments(); } $parameters = $variant->getParameters(); @@ -2557,12 +2705,11 @@ private function createFirstClassCallable( $variant->isVariadic(), $variant->getTemplateTypeMap(), $variant->getResolvedTemplateTypeMap(), - $variant instanceof ParametersAcceptorWithPhpDocs ? $variant->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $variant instanceof ExtendedParametersAcceptor ? $variant->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), $templateTags, $throwPoints, $impurePoints, - [], - [], + acceptsNamedArguments: $acceptsNamedArguments, ); } @@ -2583,10 +2730,6 @@ public function getKeepVoidType(Expr $node): Type return $this->getType($clonedNode); } - /** - * @api - * @deprecated Use getNativeType() - */ public function doNotTreatPhpDocTypesAsCertain(): Scope { return $this->promoteNativeTypes(); @@ -2625,7 +2768,7 @@ private function promoteNativeTypes(): self /** * @param Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ - private function hasPropertyNativeType($propertyFetch): bool + public function hasPropertyNativeType($propertyFetch): bool { $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyFetch, $this); if ($propertyReflection === null) { @@ -2636,11 +2779,10 @@ private function hasPropertyNativeType($propertyFetch): bool return false; } - return !$propertyReflection->getNativeType() instanceof MixedType; + return $propertyReflection->hasNativeType(); } - /** @api */ - protected function getTypeFromArrayDimFetch( + private function getTypeFromArrayDimFetch( Expr\ArrayDimFetch $arrayDimFetch, Type $offsetType, Type $offsetAccessibleType, @@ -2665,11 +2807,9 @@ protected function getTypeFromArrayDimFetch( 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; @@ -2688,7 +2828,7 @@ private function resolveExactName(Name $name): ?string return null; } - return $originalClass; + return $name; } /** @api */ @@ -2748,22 +2888,33 @@ public function resolveTypeByName(Name $name): TypeWithClassName return new ObjectType($originalClass); } - /** - * @api - * @param mixed $value - */ - public function getTypeFromValue($value): Type + private function resolveTypeByNameWithLateStaticBinding(Name $class, Node\Identifier $name): TypeWithClassName { - return ConstantTypeHelper::getTypeFromValue($value); + $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 - * @deprecated use hasExpressionType instead + * @param mixed $value */ - public function isSpecified(Expr $node): bool + public function getTypeFromValue($value): Type { - return !$node instanceof Variable && $this->hasExpressionType($node)->yes(); + return ConstantTypeHelper::getTypeFromValue($value); } /** @api */ @@ -2956,6 +3107,7 @@ public function enterClassMethod( array $parameterOutTypes = [], array $immediatelyInvokedCallableParameters = [], array $phpDocClosureThisTypeParameters = [], + bool $isConstructor = false, ): self { if (!$this->isInClass()) { @@ -2966,28 +3118,119 @@ public function enterClassMethod( new PhpMethodFromParserNodeReflection( $this->getClassReflection(), $classMethod, + null, $this->getFile(), $templateTypeMap, $this->getRealParameterTypes($classMethod), array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocParameterTypes), $this->getRealParameterDefaultValues($classMethod), + $this->getParameterAttributes($classMethod), $this->transformStaticType($this->getFunctionType($classMethod->returnType, false, false)), $phpDocReturnType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocReturnType)) : null, - $throwType, + $throwType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($throwType)) : null, + $deprecatedDescription, + $isDeprecated, + $isInternal, + $isFinal, + $isPure, + $acceptsNamedArguments, + $asserts ?? Assertions::createEmpty(), + $selfOutType, + $phpDocComment, + array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $parameterOutTypes), + $immediatelyInvokedCallableParameters, + array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocClosureThisTypeParameters), + $isConstructor, + $this->attributeReflectionFactory->fromAttrGroups($classMethod->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $classMethod)), + ), + !$classMethod->isStatic(), + ); + } + + /** + * @param Type[] $phpDocParameterTypes + */ + public function enterPropertyHook( + Node\PropertyHook $hook, + string $propertyName, + Identifier|Name|ComplexType|null $nativePropertyTypeNode, + ?Type $phpDocPropertyType, + array $phpDocParameterTypes, + ?Type $throwType, + ?string $deprecatedDescription, + bool $isDeprecated, + ?string $phpDocComment, + ): self + { + if (!$this->isInClass()) { + throw new ShouldNotHappenException(); + } + + $phpDocParameterTypes = array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocParameterTypes); + + $hookName = $hook->name->toLowerString(); + if ($hookName === 'set') { + if ($hook->params === []) { + $hook = clone $hook; + $hook->params = [ + new Node\Param(new Variable('value'), 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, - $isInternal, - $isFinal, - $isPure, - $acceptsNamedArguments, - $asserts ?? Assertions::createEmpty(), - $selfOutType, + false, + false, + false, + true, + Assertions::createEmpty(), + null, $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), + [], + [], + [], + false, + $this->attributeReflectionFactory->fromAttrGroups($hook->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $hook)), ), - !$classMethod->isStatic(), + true, ); } @@ -3000,7 +3243,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); @@ -3049,6 +3292,27 @@ private function getRealParameterDefaultValues(Node\FunctionLike $functionLike): return $realParameterDefaultValues; } + /** + * @return array> + */ + private function getParameterAttributes(ClassMethod|Function_|PropertyHook $functionLike): array + { + $parameterAttributes = []; + $className = null; + if ($this->isInClass()) { + $className = $this->getClassReflection()->getName(); + } + foreach ($functionLike->getParams() as $parameter) { + if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { + throw new ShouldNotHappenException(); + } + + $parameterAttributes[$parameter->var->name] = $this->attributeReflectionFactory->fromAttrGroups($parameter->attrGroups, InitializerExprContext::fromStubParameter($className, $this->getFile(), $functionLike)); + } + + return $parameterAttributes; + } + /** * @api * @param Type[] $phpDocParameterTypes @@ -3065,7 +3329,6 @@ public function enterFunction( ?string $deprecatedDescription, bool $isDeprecated, bool $isInternal, - bool $isFinal, ?bool $isPure = null, bool $acceptsNamedArguments = true, ?Assertions $asserts = null, @@ -3083,13 +3346,13 @@ public function enterFunction( $this->getRealParameterTypes($function), array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $phpDocParameterTypes), $this->getRealParameterDefaultValues($function), + $this->getParameterAttributes($function), $this->getFunctionType($function->returnType, $function->returnType === null, false), $phpDocReturnType !== null ? TemplateTypeHelper::toArgument($phpDocReturnType) : null, $throwType, $deprecatedDescription, $isDeprecated, $isInternal, - $isFinal, $isPure, $acceptsNamedArguments, $asserts ?? Assertions::createEmpty(), @@ -3097,6 +3360,7 @@ public function enterFunction( 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, ); @@ -3104,20 +3368,25 @@ public function enterFunction( private function enterFunctionLike( PhpFunctionFromParserNodeReflection $functionReflection, - bool $preserveThis, + bool $preserveConstructorScope, ): self { - $acceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()); $parametersByName = []; - foreach ($acceptor->getParameters() as $parameter) { + foreach ($functionReflection->getParameters() as $parameter) { $parametersByName[$parameter->getName()] = $parameter; } $expressionTypes = []; $nativeExpressionTypes = []; $conditionalTypes = []; - foreach ($acceptor->getParameters() as $parameter) { + + if ($preserveConstructorScope) { + $expressionTypes = $this->rememberConstructorExpressions($this->expressionTypes); + $nativeExpressionTypes = $this->rememberConstructorExpressions($this->nativeExpressionTypes); + } + + foreach ($functionReflection->getParameters() as $parameter) { $parameterType = $parameter->getType(); if ($parameterType instanceof ConditionalTypeForParameter) { @@ -3142,10 +3411,10 @@ private function enterFunctionLike( $paramExprString = '$' . $parameter->getName(); if ($parameter->isVariadic()) { - if ($this->phpVersion->supportsNamedArguments() && $functionReflection->acceptsNamedArguments()) { + if (!$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) { $parameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $parameterType); } else { - $parameterType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $parameterType)); + $parameterType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $parameterType), new AccessoryArrayListType()); } } $parameterNode = new Variable($parameter->getName()); @@ -3157,23 +3426,16 @@ private function enterFunctionLike( $nativeParameterType = $parameter->getNativeType(); if ($parameter->isVariadic()) { - if ($this->phpVersion->supportsNamedArguments() && $functionReflection->acceptsNamedArguments()) { + if (!$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) { $nativeParameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $nativeParameterType); } else { - $nativeParameterType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $nativeParameterType)); + $nativeParameterType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $nativeParameterType), new AccessoryArrayListType()); } } $nativeExpressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameterNode, $nativeParameterType); $nativeExpressionTypes[$parameterOriginalValueExprString] = ExpressionTypeHolder::createYes($parameterOriginalValueExpr, $nativeParameterType); } - if ($preserveThis && array_key_exists('$this', $this->expressionTypes)) { - $expressionTypes['$this'] = $this->expressionTypes['$this']; - } - if ($preserveThis && array_key_exists('$this', $this->nativeExpressionTypes)) { - $nativeExpressionTypes['$this'] = $this->nativeExpressionTypes['$this']; - } - return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), @@ -3346,7 +3608,7 @@ public function isInClosureBind(): bool */ public function enterAnonymousFunction( Expr\Closure $closure, - ?array $callableParameters = null, + ?array $callableParameters, ): self { $anonymousFunctionReflection = $this->getType($closure); @@ -3381,7 +3643,7 @@ public function enterAnonymousFunction( */ private function enterAnonymousFunctionWithoutReflection( Expr\Closure $closure, - ?array $callableParameters = null, + ?array $callableParameters, ): self { $expressionTypes = []; @@ -3447,9 +3709,6 @@ private function enterAnonymousFunctionWithoutReflection( continue; } foreach ($variables as $variable) { - if (!$variable instanceof Variable) { - continue 2; - } if (!is_string($variable->name)) { continue 2; } @@ -3526,7 +3785,7 @@ private function invalidateStaticExpressions(array $expressionTypes): array * @api * @param ParameterReflection[]|null $callableParameters */ - public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $callableParameters = null): self + public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $callableParameters): self { $anonymousFunctionReflection = $this->getType($arrowFunction); if (!$anonymousFunctionReflection instanceof ClosureType) { @@ -3587,7 +3846,7 @@ private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFu if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { throw new ShouldNotHappenException(); } - $arrowFunctionScope = $arrowFunctionScope->assignVariable($parameter->var->name, $parameterType, $parameterType); + $arrowFunctionScope = $arrowFunctionScope->assignVariable($parameter->var->name, $parameterType, $parameterType, TrinaryLogic::createYes()); } if ($arrowFunction->static) { @@ -3635,7 +3894,7 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type ); } if ($isVariadic) { - if ($this->phpVersion->supportsNamedArguments()) { + if (!$this->getPhpVersion()->supportsNamedArguments()->no()) { return new ArrayType(new UnionType([new IntegerType(), new StringType()]), $this->getFunctionType( $type, false, @@ -3643,11 +3902,11 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type )); } - return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $this->getFunctionType( + return TypeCombinator::intersect(new ArrayType(new IntegerType(), $this->getFunctionType( $type, false, false, - ))); + )), new AccessoryArrayListType()); } if ($type instanceof Name) { @@ -3706,6 +3965,7 @@ public function enterForeach(self $originalScope, Expr $iteratee, string $valueN $valueName, $originalScope->getIterableValueType($iterateeType), $originalScope->getIterableValueType($nativeIterateeType), + TrinaryLogic::createYes(), ); if ($keyName !== null) { $scope = $scope->enterForeachKey($originalScope, $iteratee, $keyName); @@ -3722,6 +3982,7 @@ public function enterForeachKey(self $originalScope, Expr $iteratee, string $key $keyName, $originalScope->getIterableKeyType($iterateeType), $originalScope->getIterableKeyType($nativeIterateeType), + TrinaryLogic::createYes(), ); if ($iterateeType->isArray()->yes()) { @@ -3735,17 +3996,6 @@ public function enterForeachKey(self $originalScope, Expr $iteratee, string $key return $scope; } - /** - * @deprecated Use enterCatchType - * @param Node\Name[] $classes - */ - public function enterCatch(array $classes, ?string $variableName): self - { - $type = TypeCombinator::union(...array_map(static fn (Node\Name $class): ObjectType => new ObjectType((string) $class), $classes)); - - return $this->enterCatchType($type, $variableName); - } - public function enterCatchType(Type $catchType, ?string $variableName): self { if ($variableName === null) { @@ -3756,6 +4006,7 @@ public function enterCatchType(Type $catchType, ?string $variableName): self $variableName, TypeCombinator::intersect($catchType, new ObjectType(Throwable::class)), TypeCombinator::intersect($catchType, new ObjectType(Throwable::class)), + TrinaryLogic::createYes(), ); } @@ -3830,7 +4081,7 @@ public function isInExpressionAssign(Expr $expr): bool public function setAllowedUndefinedExpression(Expr $expr): self { - if ($this->phpVersion->deprecatesDynamicProperties() && $expr instanceof Expr\StaticPropertyFetch) { + if ($expr instanceof Expr\StaticPropertyFetch) { return $this; } @@ -3901,18 +4152,16 @@ public function isUndefinedExpressionAllowed(Expr $expr): bool return array_key_exists($exprString, $this->currentlyAllowedUndefinedExpressions); } - public function assignVariable(string $variableName, Type $type, Type $nativeType, ?TrinaryLogic $certainty = null): self + public function assignVariable(string $variableName, Type $type, Type $nativeType, TrinaryLogic $certainty): self { $node = new Variable($variableName); $scope = $this->assignExpression($node, $type, $nativeType); - if ($certainty !== null) { - 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); - } + 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)); @@ -3960,7 +4209,7 @@ public function unsetExpression(Expr $expr): self return $scope->invalidateExpression($expr); } - public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, ?TrinaryLogic $certainty = null): self + public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, TrinaryLogic $certainty): self { if ($expr instanceof ConstFetch) { $loweredConstName = strtolower($expr->name->toString()); @@ -4008,9 +4257,7 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, } } - if ($certainty === null) { - $certainty = TrinaryLogic::createYes(); - } elseif ($certainty->no()) { + if ($certainty->no()) { throw new ShouldNotHappenException(); } @@ -4046,11 +4293,8 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, return $scope; } - public function assignExpression(Expr $expr, Type $type, ?Type $nativeType = null): self + public function assignExpression(Expr $expr, Type $type, Type $nativeType): self { - if ($nativeType === null) { - $nativeType = new MixedType(); - } $scope = $this; if ($expr instanceof PropertyFetch) { $scope = $this->invalidateExpression($expr) @@ -4061,7 +4305,7 @@ public function assignExpression(Expr $expr, Type $type, ?Type $nativeType = nul $scope = $this->invalidateExpression($expr); } - return $scope->specifyExpressionType($expr, $type, $nativeType); + return $scope->specifyExpressionType($expr, $type, $nativeType, TrinaryLogic::createYes()); } public function assignInitializedProperty(Type $fetchedOnType, string $propertyName): self @@ -4166,6 +4410,17 @@ private function shouldInvalidateExpression(string $exprStringToInvalidate, 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; } @@ -4265,13 +4520,18 @@ public function addTypeToExpression(Expr $expr, Type $type): self if ($originalExprType->equals($nativeType)) { $newType = TypeCombinator::intersect($type, $originalExprType); - return $this->specifyExpressionType($expr, $newType, $newType); + 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(), ); } @@ -4288,6 +4548,7 @@ public function removeTypeFromExpression(Expr $expr, Type $typeToRemove): self $expr, TypeCombinator::remove($exprType, $typeToRemove), TypeCombinator::remove($this->getNativeType($expr), $typeToRemove), + TrinaryLogic::createYes(), ); } @@ -4668,10 +4929,22 @@ private function createConditionalExpressions( private function mergeVariableHolders(array $ourVariableTypeHolders, array $theirVariableTypeHolders): array { $intersectedVariableTypeHolders = []; + $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 { + $expr = $variableTypeHolder->getExpr(); + if ($nodeFinder->findFirst($expr, $globalVariableCallback) !== null) { + continue; + } + $intersectedVariableTypeHolders[$exprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); } } @@ -4681,6 +4954,11 @@ private function mergeVariableHolders(array $ourVariableTypeHolders, array $thei continue; } + $expr = $variableTypeHolder->getExpr(); + if ($nodeFinder->findFirst($expr, $globalVariableCallback) !== null) { + continue; + } + $intersectedVariableTypeHolders[$exprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); } @@ -4777,7 +5055,7 @@ private function processFinallyScopeVariableTypeHolders( } /** - * @param Expr\ClosureUse[] $byRefUses + * @param Node\ClosureUse[] $byRefUses */ public function processClosureScope( self $closureScope, @@ -4812,7 +5090,7 @@ public function processClosureScope( $prevVariableType = $prevScope->getVariableType($variableName); if (!$variableType->equals($prevVariableType)) { $variableType = TypeCombinator::union($variableType, $prevVariableType); - $variableType = self::generalizeType($variableType, $prevVariableType, 0); + $variableType = $this->generalizeType($variableType, $prevVariableType, 0); } } @@ -4937,7 +5215,7 @@ private function generalizeVariableTypeHolders( $variableTypeHolders[$variableExprString] = new ExpressionTypeHolder( $variableTypeHolder->getExpr(), - self::generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$variableExprString]->getType(), 0), + $this->generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$variableExprString]->getType(), 0), $variableTypeHolder->getCertainty(), ); } @@ -4945,7 +5223,7 @@ private function generalizeVariableTypeHolders( return $variableTypeHolders; } - private static function generalizeType(Type $a, Type $b, int $depth): Type + private function generalizeType(Type $a, Type $b, int $depth): Type { if ($a->equals($b)) { return $a; @@ -5030,12 +5308,15 @@ private static function generalizeType(Type $a, Type $b, int $depth): Type } else { $constantArraysA = TypeCombinator::union(...$constantArrays['a']); $constantArraysB = TypeCombinator::union(...$constantArrays['b']); - if ($constantArraysA->getIterableKeyType()->equals($constantArraysB->getIterableKeyType())) { + if ( + $constantArraysA->getIterableKeyType()->equals($constantArraysB->getIterableKeyType()) + && $constantArraysA->getArraySize()->getGreaterOrEqualType($this->phpVersion)->isSuperTypeOf($constantArraysB->getArraySize())->yes() + ) { $resultArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach (TypeUtils::flattenTypes($constantArraysA->getIterableKeyType()) as $keyType) { $resultArrayBuilder->setOffsetValueType( $keyType, - self::generalizeType( + $this->generalizeType( $constantArraysA->getOffsetValueType($keyType), $constantArraysB->getOffsetValueType($keyType), $depth + 1, @@ -5047,14 +5328,18 @@ private static function generalizeType(Type $a, Type $b, int $depth): Type $resultTypes[] = $resultArrayBuilder->getArray(); } else { $resultType = new ArrayType( - TypeCombinator::union(self::generalizeType($constantArraysA->getIterableKeyType(), $constantArraysB->getIterableKeyType(), $depth + 1)), - TypeCombinator::union(self::generalizeType($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType(), $depth + 1)), + 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()) { + 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 = AccessoryArrayListType::intersectWith($resultType); + $resultType = TypeCombinator::intersect($resultType, new AccessoryArrayListType()); } $resultTypes[] = $resultType; } @@ -5090,14 +5375,14 @@ private static function generalizeType(Type $a, Type $b, int $depth): Type } $resultType = new ArrayType( - TypeCombinator::union(self::generalizeType($generalArraysA->getIterableKeyType(), $generalArraysB->getIterableKeyType(), $depth + 1)), - TypeCombinator::union(self::generalizeType($aValueType, $bValueType, $depth + 1)), + 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 = AccessoryArrayListType::intersectWith($resultType); + $resultType = TypeCombinator::intersect($resultType, new AccessoryArrayListType()); } if ($generalArraysA->isOversizedArray()->yes() && $generalArraysB->isOversizedArray()->yes()) { $resultType = TypeCombinator::intersect($resultType, new OversizedArrayType()); @@ -5253,11 +5538,11 @@ private static function generalizeType(Type $a, Type $b, int $depth): Type private static function getArrayDepth(Type $type): int { $depth = 0; - $arrays = TypeUtils::getAnyArrays($type); + $arrays = TypeUtils::toBenevolentUnion($type)->getArrays(); while (count($arrays) > 0) { $temp = $type->getIterableValueType(); $type = $temp; - $arrays = TypeUtils::getAnyArrays($type); + $arrays = TypeUtils::toBenevolentUnion($type)->getArrays(); $depth++; } @@ -5318,12 +5603,67 @@ private function getBooleanExpressionDepth(Expr $expr, int $depth = 0): int return $depth; } - /** @api */ + /** + * @api + * @deprecated Use canReadProperty() or canWriteProperty() + */ public function canAccessProperty(PropertyReflection $propertyReflection): bool { return $this->canAccessClassMember($propertyReflection); } + /** @api */ + public function canReadProperty(ExtendedPropertyReflection $propertyReflection): bool + { + return $this->canAccessClassMember($propertyReflection); + } + + /** @api */ + public function canWriteProperty(ExtendedPropertyReflection $propertyReflection): bool + { + if (!$propertyReflection->isPrivateSet() && !$propertyReflection->isProtectedSet()) { + return $this->canAccessClassMember($propertyReflection); + } + + if (!$this->phpVersion->supportsAsymmetricVisibility()) { + return $this->canAccessClassMember($propertyReflection); + } + + $propertyDeclaringClass = $propertyReflection->getDeclaringClass(); + $canAccessClassMember = static function (ClassReflection $classReflection) use ($propertyReflection, $propertyDeclaringClass) { + if ($propertyReflection->isPrivateSet()) { + return $classReflection->getName() === $propertyDeclaringClass->getName(); + } + + // protected set + + if ( + $classReflection->getName() === $propertyDeclaringClass->getName() + || $classReflection->isSubclassOfClass($propertyDeclaringClass) + ) { + return true; + } + + return $propertyReflection->getDeclaringClass()->isSubclassOfClass($classReflection); + }; + + foreach ($this->inClosureBindScopeClasses as $inClosureBindScopeClass) { + if (!$this->reflectionProvider->hasClass($inClosureBindScopeClass)) { + continue; + } + + if ($canAccessClassMember($this->reflectionProvider->getClass($inClosureBindScopeClass))) { + return true; + } + } + + if ($this->isInClass()) { + return $canAccessClassMember($this->getClassReflection()); + } + + return false; + } + /** @api */ public function canCallMethod(MethodReflection $methodReflection): bool { @@ -5335,7 +5675,7 @@ public function canCallMethod(MethodReflection $methodReflection): bool } /** @api */ - public function canAccessConstant(ConstantReflection $constantReflection): bool + public function canAccessConstant(ClassConstantReflection $constantReflection): bool { return $this->canAccessClassMember($constantReflection); } @@ -5346,22 +5686,22 @@ private function canAccessClassMember(ClassMemberReflection $classMemberReflecti return true; } - $classReflectionName = $classMemberReflection->getDeclaringClass()->getName(); - $canAccessClassMember = static function (ClassReflection $classReflection) use ($classMemberReflection, $classReflectionName) { + $classMemberDeclaringClass = $classMemberReflection->getDeclaringClass(); + $canAccessClassMember = static function (ClassReflection $classReflection) use ($classMemberReflection, $classMemberDeclaringClass) { if ($classMemberReflection->isPrivate()) { - return $classReflection->getName() === $classReflectionName; + return $classReflection->getName() === $classMemberDeclaringClass->getName(); } // protected if ( - $classReflection->getName() === $classReflectionName - || $classReflection->isSubclassOf($classReflectionName) + $classReflection->getName() === $classMemberDeclaringClass->getName() + || $classReflection->isSubclassOfClass($classMemberDeclaringClass) ) { return true; } - return $classMemberReflection->getDeclaringClass()->isSubclassOf($classReflection->getName()); + return $classMemberReflection->getDeclaringClass()->isSubclassOfClass($classReflection); }; foreach ($this->inClosureBindScopeClasses as $inClosureBindScopeClass) { @@ -5392,7 +5732,7 @@ public function debug(): array $descriptions[$key] = $variableTypeHolder->getType()->describe(VerbosityLevel::precise()); } foreach ($this->nativeExpressionTypes as $exprString => $nativeTypeHolder) { - $key = sprintf('native %s', $exprString); + $key = sprintf('native %s (%s)', $exprString, $nativeTypeHolder->getCertainty()->describe()); $descriptions[$key] = $nativeTypeHolder->getType()->describe(VerbosityLevel::precise()); } @@ -5422,9 +5762,18 @@ public function debug(): array */ 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)) { @@ -5432,12 +5781,20 @@ private function exactInstantiation(New_ $node, string $className): ?Type } $classReflection = $this->reflectionProvider->getClass($resolvedClassName); + $nonFinalClassReflection = $classReflection; + if (!$isStatic) { + $classReflection = $classReflection->asFinal(); + } if ($classReflection->hasConstructor()) { $constructorMethod = $classReflection->getConstructor(); } else { $constructorMethod = new DummyConstructorReflection($classReflection); } + if ($constructorMethod->getName() === '') { + throw new ShouldNotHappenException(); + } + $resolvedTypes = []; $methodCall = new Expr\StaticCall( new Name($resolvedClassName), @@ -5481,14 +5838,14 @@ 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; } $assignedToProperty = $node->getAttribute(NewAssignedToPropertyVisitor::ATTRIBUTE_NAME); if ($assignedToProperty !== null) { - $constructorVariant = ParametersAcceptorSelector::selectSingle($constructorMethod->getVariants()); + $constructorVariant = $constructorMethod->getOnlyVariant(); $classTemplateTypes = $classReflection->getTemplateTypeMap()->getTypes(); $originalClassTemplateTypes = $classTemplateTypes; foreach ($constructorVariant->getParameters() as $parameter) { @@ -5507,86 +5864,195 @@ private function exactInstantiation(New_ $node, string $className): ?Type if (count($classTemplateTypes) === count($originalClassTemplateTypes)) { $propertyType = TypeCombinator::removeNull($this->getType($assignedToProperty)); - if ($objectType->isSuperTypeOf($propertyType)->yes()) { + $nonFinalObjectType = $isStatic ? new StaticType($nonFinalClassReflection) : new ObjectType($resolvedClassName, classReflection: $nonFinalClassReflection); + if ($nonFinalObjectType->isSuperTypeOf($propertyType)->yes()) { return $propertyType; } } } - if ($constructorMethod instanceof DummyConstructorReflection || $constructorMethod->getDeclaringClass()->getName() !== $classReflection->getName()) { + if ($constructorMethod instanceof DummyConstructorReflection) { + if ($isStatic) { + return new GenericStaticType( + $classReflection, + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + null, + [], + ); + } + + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()); return new GenericObjectType( $resolvedClassName, - $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + $types, + classReflection: $classReflection->withTypes($types)->asFinal(), ); } - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $this, - $methodCall->getArgs(), - $constructorMethod->getVariants(), - $constructorMethod->getNamedArgumentsVariants(), - ); + if ($constructorMethod->getDeclaringClass()->getName() !== $classReflection->getName()) { + if (!$constructorMethod->getDeclaringClass()->isGeneric()) { + if ($isStatic) { + return new GenericStaticType( + $classReflection, + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + null, + [], + ); + } - if ($this->explicitMixedInUnknownGenericNew) { - $resolvedTemplateTypeMap = $parametersAcceptor->getResolvedTemplateTypeMap(); - return TypeTraverser::map(new GenericObjectType( - $resolvedClassName, - $classReflection->typeMapToList($classReflection->getTemplateTypeMap()), - ), static function (Type $type, callable $traverse) use ($resolvedTemplateTypeMap): Type { - if ($type instanceof TemplateType && !$type->isArgument()) { - $newType = $resolvedTemplateTypeMap->getType($type->getName()); - if ($newType === null || $newType instanceof ErrorType) { - return $type->getBound(); - } + $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, + [], + ); + } - return TemplateTypeHelper::generalizeInferredTemplateType($type, $newType); + $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, + [], + ); } - return $traverse($type); - }); - } + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()); + return new GenericObjectType( + $resolvedClassName, + $types, + classReflection: $classReflection->withTypes($types)->asFinal(), + ); + } - $resolvedPhpDoc = $classReflection->getResolvedPhpDoc(); - if ($resolvedPhpDoc === null) { - return $objectType; - } + $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, + [], + ); + } - $list = []; - $typeMap = $parametersAcceptor->getResolvedTemplateTypeMap(); - foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { - $templateType = $typeMap->getType($tag->getName()); - if ($templateType !== null) { - $list[] = $templateType; - continue; + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()); + return new GenericObjectType( + $resolvedClassName, + $types, + 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); } - $bound = $tag->getBound(); - if ($bound instanceof MixedType && $bound->isExplicitMixed()) { - $bound = new MixedType(false); + + if ($isStatic) { + return new GenericStaticType( + $classReflection, + $classReflection->typeMapToList(new TemplateTypeMap($resolvedTypeMap)), + null, + [], + ); } - $list[] = $bound; + + $types = $classReflection->typeMapToList(new TemplateTypeMap($resolvedTypeMap)); + return new GenericObjectType( + $resolvedClassName, + $types, + classReflection: $classReflection->withTypes($types)->asFinal(), + ); } - return new GenericObjectType( + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $this, + $methodCall->getArgs(), + $constructorMethod->getVariants(), + $constructorMethod->getNamedArgumentsVariants(), + ); + + $resolvedTemplateTypeMap = $parametersAcceptor->getResolvedTemplateTypeMap(); + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()); + $newGenericType = new GenericObjectType( $resolvedClassName, - $list, + $types, + classReflection: $classReflection->withTypes($types)->asFinal(), ); + if ($isStatic) { + $newGenericType = new GenericStaticType( + $classReflection, + $types, + null, + [], + ); + } + return TypeTraverser::map($newGenericType, static function (Type $type, callable $traverse) use ($resolvedTemplateTypeMap): Type { + if ($type instanceof TemplateType && !$type->isArgument()) { + $newType = $resolvedTemplateTypeMap->getType($type->getName()); + if ($newType === null || $newType instanceof ErrorType) { + return $type->getDefault() ?? $type->getBound(); + } + + return TemplateTypeHelper::generalizeInferredTemplateType($type, $newType); + } + + return $traverse($type); + }); } private function filterTypeWithMethod(Type $typeWithMethod, string $methodName): ?Type { 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); + $typeWithMethod = $typeWithMethod->filterTypes(static fn (Type $innerType) => $innerType->hasMethod($methodName)->yes()); } if (!$typeWithMethod->hasMethod($methodName)->yes()) { @@ -5607,7 +6073,6 @@ public function getMethodReflection(Type $typeWithMethod, string $methodName): ? return $type->getMethod($methodName, $this); } - /** @api */ public function getNakedMethod(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection { $type = $this->filterTypeWithMethod($typeWithMethod, $methodName); @@ -5687,21 +6152,10 @@ private function methodCallReturnType(Type $typeWithMethod, string $methodName, } /** @api */ - public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?PropertyReflection + public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection { if ($typeWithProperty instanceof UnionType) { - $newTypes = []; - foreach ($typeWithProperty->getTypes() as $innerType) { - if (!$innerType->hasProperty($propertyName)->yes()) { - continue; - } - - $newTypes[] = $innerType; - } - if (count($newTypes) === 0) { - return null; - } - $typeWithProperty = TypeCombinator::union(...$newTypes); + $typeWithProperty = $typeWithProperty->filterTypes(static fn (Type $innerType) => $innerType->hasProperty($propertyName)->yes()); } if (!$typeWithProperty->hasProperty($propertyName)->yes()) { return null; @@ -5727,21 +6181,10 @@ private function propertyFetchType(Type $fetchedOnType, string $propertyName, Ex return $propertyReflection->getReadableType(); } - public function getConstantReflection(Type $typeWithConstant, string $constantName): ?ConstantReflection + public function getConstantReflection(Type $typeWithConstant, string $constantName): ?ClassConstantReflection { if ($typeWithConstant instanceof UnionType) { - $newTypes = []; - foreach ($typeWithConstant->getTypes() as $innerType) { - if (!$innerType->hasConstant($constantName)->yes()) { - continue; - } - - $newTypes[] = $innerType; - } - if (count($newTypes) === 0) { - return null; - } - $typeWithConstant = TypeCombinator::union(...$newTypes); + $typeWithConstant = $typeWithConstant->filterTypes(static fn (Type $innerType) => $innerType->hasConstant($constantName)->yes()); } if (!$typeWithConstant->hasConstant($constantName)->yes()) { return null; @@ -5766,6 +6209,25 @@ private function getConstantTypes(): array return $constantTypes; } + private function getGlobalConstantType(Name $name): ?Type + { + $fetches = []; + if (!$name->isFullyQualified() && $this->getNamespace() !== null) { + $fetches[] = new ConstFetch(new FullyQualified([$this->getNamespace(), $name->toString()])); + } + + $fetches[] = new ConstFetch(new FullyQualified($name->toString())); + $fetches[] = new ConstFetch($name); + + foreach ($fetches as $constFetch) { + if ($this->hasExpressionType($constFetch)->yes()) { + return $this->getType($constFetch); + } + } + + return null; + } + /** * @return array */ @@ -5785,18 +6247,10 @@ private function getNativeConstantTypes(): array public function getIterableKeyType(Type $iteratee): Type { if ($iteratee instanceof UnionType) { - $newTypes = []; - foreach ($iteratee->getTypes() as $innerType) { - if (!$innerType->isIterable()->yes()) { - continue; - } - - $newTypes[] = $innerType; + $filtered = $iteratee->filterTypes(static fn (Type $innerType) => $innerType->isIterable()->yes()); + if (!$filtered instanceof NeverType) { + $iteratee = $filtered; } - if (count($newTypes) === 0) { - return $iteratee->getIterableKeyType(); - } - $iteratee = TypeCombinator::union(...$newTypes); } return $iteratee->getIterableKeyType(); @@ -5805,21 +6259,36 @@ public function getIterableKeyType(Type $iteratee): Type public function getIterableValueType(Type $iteratee): Type { if ($iteratee instanceof UnionType) { - $newTypes = []; - foreach ($iteratee->getTypes() as $innerType) { - if (!$innerType->isIterable()->yes()) { - continue; - } - - $newTypes[] = $innerType; + $filtered = $iteratee->filterTypes(static fn (Type $innerType) => $innerType->isIterable()->yes()); + if (!$filtered instanceof NeverType) { + $iteratee = $filtered; } - if (count($newTypes) === 0) { - return $iteratee->getIterableValueType(); - } - $iteratee = TypeCombinator::union(...$newTypes); } return $iteratee->getIterableValueType(); } + public function getPhpVersion(): PhpVersions + { + $constType = $this->getGlobalConstantType(new Name('PHP_VERSION_ID')); + + $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; + } + + if ($constType !== null && !$isOverallPhpVersionRange) { + return new PhpVersions($constType); + } + + if (is_array($this->configPhpVersion)) { + return new PhpVersions(IntegerRangeType::fromInterval($this->configPhpVersion['min'], $this->configPhpVersion['max'])); + } + return new PhpVersions(new ConstantIntegerType($this->phpVersion->getVersionId())); + } + } diff --git a/src/Analyser/NameScope.php b/src/Analyser/NameScope.php index e083daf91b..2ce18d91be 100644 --- a/src/Analyser/NameScope.php +++ b/src/Analyser/NameScope.php @@ -16,8 +16,10 @@ use function str_starts_with; use function strtolower; -/** @api */ -class NameScope +/** + * @api + */ +final class NameScope { private TemplateTypeMap $templateTypeMap; @@ -172,6 +174,20 @@ public function withTemplateTypeMap(TemplateTypeMap $map): self ); } + public function withoutNamespaceAndUses(): self + { + return new self( + null, + [], + $this->className, + $this->functionName, + $this->templateTypeMap, + $this->typeAliasesMap, + $this->bypassTypeAliases, + $this->constUses, + ); + } + public function withClassName(string $className): self { return new self( @@ -220,21 +236,4 @@ public function hasTypeAlias(string $alias): bool return array_key_exists($alias, $this->typeAliasesMap); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['namespace'], - $properties['uses'], - $properties['className'], - $properties['functionName'], - $properties['templateTypeMap'], - $properties['typeAliasesMap'], - $properties['bypassTypeAliases'], - $properties['constUses'], - ); - } - } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 64d0dd9ec1..0c25c7271b 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5,14 +5,16 @@ 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; @@ -35,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_; @@ -48,7 +51,6 @@ use PhpParser\Node\Stmt\Return_; use PhpParser\Node\Stmt\Static_; 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_; @@ -61,6 +63,8 @@ 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\ParameterClosureTypeExtensionProvider; @@ -99,6 +103,7 @@ 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; @@ -112,6 +117,8 @@ 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; @@ -119,6 +126,8 @@ 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; @@ -126,30 +135,35 @@ 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\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Reflection\Php\PhpMethodReflection; +use PHPStan\Reflection\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; @@ -157,6 +171,7 @@ 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; @@ -171,6 +186,7 @@ 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; @@ -208,10 +224,12 @@ use function str_starts_with; use function strtolower; use function trim; +use function usort; use const PHP_VERSION_ID; use const SORT_NUMERIC; -class NodeScopeResolver +#[AutowiredService] +final class NodeScopeResolver { private const LOOP_SCOPE_ITERATIONS = 3; @@ -237,14 +255,18 @@ class NodeScopeResolver public function __construct( 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, @@ -252,16 +274,23 @@ public function __construct( private readonly ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider, 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 $detectDeadTypeInMultiCatch, - private readonly bool $paramOutType, - private readonly bool $preciseMissingReturn, + private readonly bool $narrowMethodScopeFromConstructor = true, ) { $earlyTerminatingMethodNames = []; @@ -309,13 +338,38 @@ public function processNodes( } $alreadyTerminated = true; - $nextStmt = $this->getFirstUnreachableNode(array_slice($nodes, $i + 1), true); - if (!$nextStmt instanceof Node\Stmt) { + $nextStmts = $this->getNextUnreachableStatements(array_slice($nodes, $i + 1), true); + $this->processUnreachableStatement($nextStmts, $scope, $nodeCallback); + } + } + + /** + * @param Node\Stmt[] $nextStmts + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processUnreachableStatement(array $nextStmts, MutatingScope $scope, callable $nodeCallback): void + { + if ($nextStmts === []) { + return; + } + + $unreachableStatement = null; + $nextStatements = []; + + foreach ($nextStmts as $key => $nextStmt) { + if ($key === 0) { + $unreachableStatement = $nextStmt; continue; } - $nodeCallback(new UnreachableStatementNode($nextStmt), $scope); + $nextStatements[] = $nextStmt; + } + + if (!$unreachableStatement instanceof Node\Stmt) { + return; } + + $nodeCallback(new UnreachableStatementNode($unreachableStatement, $nextStatements), $scope); } /** @@ -328,12 +382,9 @@ public function processStmtNodes( array $stmts, MutatingScope $scope, callable $nodeCallback, - ?StatementContext $context = null, + StatementContext $context, ): StatementResult { - if ($context === null) { - $context = StatementContext::createTopLevel(); - } $exitPoints = []; $throwPoints = []; $impurePoints = []; @@ -342,6 +393,7 @@ public function processStmtNodes( $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)) { @@ -359,11 +411,8 @@ public function processStmtNodes( $hasYield = $hasYield || $statementResult->hasYield(); if ($shouldCheckLastStatement && $isLast) { - /** @var Node\Stmt\Function_|Node\Stmt\ClassMethod|Expr\Closure $parentNode */ - $parentNode = $parentNode; - $endStatements = $statementResult->getEndStatements(); - if ($this->preciseMissingReturn && count($endStatements) > 0) { + if (count($endStatements) > 0) { foreach ($endStatements as $endStatement) { $endStatementResult = $endStatement->getResult(); $nodeCallback(new ExecutionEndNode( @@ -376,7 +425,7 @@ public function processStmtNodes( $endStatementResult->getThrowPoints(), $endStatementResult->getImpurePoints(), ), - $parentNode->returnType !== null, + $parentNode->getReturnType() !== null, ), $endStatementResult->getScope()); } } else { @@ -390,7 +439,7 @@ public function processStmtNodes( $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), ), - $parentNode->returnType !== null, + $parentNode->getReturnType() !== null, ), $scope); } } @@ -404,18 +453,13 @@ public function processStmtNodes( } $alreadyTerminated = true; - $nextStmt = $this->getFirstUnreachableNode(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_); - if ($nextStmt === null) { - continue; - } - $nodeCallback(new UnreachableStatementNode($nextStmt), $scope); + $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, $impurePoints); if ($stmtCount === 0 && $shouldCheckLastStatement) { - /** @var Node\Stmt\Function_|Node\Stmt\ClassMethod|Expr\Closure $parentNode */ - $parentNode = $parentNode; - $returnTypeNode = $parentNode->returnType; + $returnTypeNode = $parentNode->getReturnType(); if ($parentNode instanceof Expr\Closure) { $parentNode = new Node\Stmt\Expression($parentNode, $parentNode->getAttributes()); } @@ -444,7 +488,6 @@ private function processStmtNode( && !$stmt instanceof Foreach_ && !$stmt instanceof Node\Stmt\Global_ && !$stmt instanceof Node\Stmt\Property - && !$stmt instanceof Node\Stmt\PropertyProperty && !$stmt instanceof Node\Stmt\ClassConst && !$stmt instanceof Node\Stmt\Const_ ) { @@ -473,7 +516,10 @@ private function processStmtNode( } $stmtScope = $scope; - if ($stmt instanceof Throw_ || $stmt instanceof Return_) { + 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); } @@ -492,7 +538,7 @@ private function processStmtNode( $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; @@ -517,7 +563,7 @@ private function processStmtNode( $throwPoints = []; $impurePoints = []; $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); - [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt); + [$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($stmt, $param, $scope, $nodeCallback); @@ -527,6 +573,10 @@ private function processStmtNode( $nodeCallback($stmt->returnType, $scope); } + if (!$isDeprecated) { + [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $stmt); + } + $functionScope = $scope->enterFunction( $stmt, $templateTypeMap, @@ -536,7 +586,6 @@ private function processStmtNode( $deprecatedDescription, $isDeprecated, $isInternal, - $isFinal, $isPure, $acceptsNamedArguments, $asserts, @@ -612,6 +661,13 @@ private function processStmtNode( $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, @@ -630,20 +686,22 @@ private function processStmtNode( $phpDocParameterOutTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, + $isConstructor, ); if (!$scope->isInClass()) { throw new ShouldNotHappenException(); } - $isFromTrait = $stmt->getAttribute('originalTraitMethodName') === '__construct'; - if ($isFromTrait || $stmt->name->toLowerString() === '__construct') { + $classReflection = $scope->getClassReflection(); + + if ($isConstructor) { foreach ($stmt->params as $param) { - if ($param->flags === 0) { + if ($param->flags === 0 && $param->hooks === []) { continue; } - if (!$param->var instanceof Variable || !is_string($param->var->name)) { + if (!$param->var instanceof Variable || !is_string($param->var->name) || $param->var->name === '') { throw new ShouldNotHappenException(); } $phpDoc = null; @@ -653,7 +711,7 @@ private function processStmtNode( $nodeCallback(new ClassPropertyNode( $param->var->name, $param->flags, - $param->type, + $param->type !== null ? ParserNodeTypeToPHPStanType::resolve($param->type, $classReflection) : null, null, $phpDoc, $phpDocParameterTypes[$param->var->name] ?? null, @@ -662,10 +720,19 @@ private function processStmtNode( $param, false, $scope->isInTrait(), - $scope->getClassReflection()->isReadOnly(), + $classReflection->isReadOnly(), false, - $scope->getClassReflection(), + $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()); } } @@ -675,7 +742,7 @@ private function processStmtNode( if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) { throw new ShouldNotHappenException(); } - $nodeCallback(new InClassMethodNode($scope->getClassReflection(), $methodReflection, $stmt), $methodScope); + $nodeCallback(new InClassMethodNode($classReflection, $methodReflection, $stmt), $methodScope); } if ($stmt->stmts !== null) { @@ -724,10 +791,8 @@ private function processStmtNode( $gatheredReturnStatements[] = new ReturnStatement($scope, $node); }, StatementContext::createTopLevel()); - $classReflection = $scope->getClassReflection(); - $methodReflection = $methodScope->getFunction(); - if (!$methodReflection instanceof ExtendedMethodReflection) { + if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) { throw new ShouldNotHappenException(); } @@ -741,21 +806,56 @@ private function processStmtNode( $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($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, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); @@ -789,6 +889,9 @@ private function processStmtNode( new StatementExitPoint($stmt, $scope), ], $overridingThrowPoints ?? $throwPoints, $impurePoints); } elseif ($stmt instanceof Node\Stmt\Expression) { + if ($stmt->expr instanceof Expr\Throw_) { + $scope = $stmtScope; + } $earlyTerminationExpr = $this->findEarlyTerminatingExpr($stmt->expr, $scope); $hasAssign = false; $currentScope = $scope; @@ -826,12 +929,14 @@ private function processStmtNode( $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, $impurePoints); } - return new StatementResult($scope, $hasYield, false, [], $overridingThrowPoints ?? $throwPoints, $impurePoints); + 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()); @@ -844,6 +949,9 @@ private function processStmtNode( } elseif ($stmt instanceof Node\Stmt\Trait_) { 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 = []; @@ -869,8 +977,27 @@ private function processStmtNode( $classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback); $this->processAttributeGroups($stmt, $stmt->attrGroups, $classScope, $classStatementsGatherer); - $this->processStmtNodes($stmt, $stmt->stmts, $classScope, $classStatementsGatherer, $context); - $nodeCallback(new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensionProvider, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classReflection), $classScope); + $classLikeStatements = $stmt->stmts; + if ($this->narrowMethodScopeFromConstructor) { + // analyze static methods first; constructor next; instance methods and property hooks last so we can carry over the scope + usort($classLikeStatements, static function ($a, $b) { + if ($a instanceof Node\Stmt\Property) { + return 1; + } + if ($b instanceof Node\Stmt\Property) { + return -1; + } + + if (!$a instanceof Node\Stmt\ClassMethod || !$b instanceof Node\Stmt\ClassMethod) { + return 0; + } + + return [!$a->isStatic(), $a->name->toLowerString() !== '__construct'] <=> [!$b->isStatic(), $b->name->toLowerString() !== '__construct']; + }); + } + + $this->processStmtNodes($stmt, $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(); @@ -881,33 +1008,44 @@ private function processStmtNode( $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) { $nodeCallback($prop, $scope); if ($prop->default !== null) { $this->processExprNode($stmt, $prop->default, $scope, $nodeCallback, ExpressionContext::createDeep()); } - [,,,,,,,,,,,,$isReadOnly, $docComment, ,,,$varTags, $isAllowedPrivateMutation] = $this->getPhpDocs($scope, $stmt); + if (!$scope->isInClass()) { throw new ShouldNotHappenException(); } $propertyName = $prop->name->toString(); - $phpDocType = null; - if (isset($varTags[0]) && count($varTags) === 1) { - $phpDocType = $varTags[0]->getType(); - } elseif (isset($varTags[$propertyName])) { - $phpDocType = $varTags[$propertyName]->getType(); + + if ($phpDocType === null) { + if (isset($varTags[$propertyName])) { + $phpDocType = $varTags[$propertyName]->getType(); + } } + + $propStmt = clone $stmt; + $propStmt->setAttributes($prop->getAttributes()); $nodeCallback( new ClassPropertyNode( $propertyName, $stmt->flags, - $stmt->type, + $nativePropertyType, $prop->default, $docComment, $phpDocType, false, false, - $prop, + $propStmt, $isReadOnly, $scope->isInTrait(), $scope->getClassReflection()->isReadOnly(), @@ -918,17 +1056,24 @@ private function processStmtNode( ); } + 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 Throw_) { - $result = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); - $throwPoints = $result->getThrowPoints(); - $throwPoints[] = ThrowPoint::createExplicit($result->getScope(), $scope->getType($stmt->expr), $stmt, false); - $impurePoints = $result->getImpurePoints(); - return new StatementResult($result->getScope(), $result->hasYield(), true, [ - new StatementExitPoint($stmt, $scope), - ], $throwPoints, $impurePoints); } elseif ($stmt instanceof If_) { $conditionType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); $ifAlwaysTrue = $conditionType->isTrue()->yes(); @@ -1313,18 +1458,18 @@ private function processStmtNode( $bodyScope = $initScope; $isIterableAtLeastOnce = TrinaryLogic::createYes(); + $lastCondExpr = $stmt->cond[count($stmt->cond) - 1] ?? null; foreach ($stmt->cond as $condExpr) { $condResult = $this->processExprNode($stmt, $condExpr, $bodyScope, static function (): void { }, ExpressionContext::createDeep()); $initScope = $condResult->getScope(); $condResultScope = $condResult->getScope(); - $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResultScope->getType($condExpr) : $condResultScope->getNativeType($condExpr))->toBoolean(); - if ($condTruthiness instanceof ConstantBooleanType) { - $condTruthinessTrinary = TrinaryLogic::createFromBoolean($condTruthiness->getValue()); - } else { - $condTruthinessTrinary = TrinaryLogic::createMaybe(); + + if ($condExpr === $lastCondExpr) { + $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResultScope->getType($condExpr) : $condResultScope->getNativeType($condExpr))->toBoolean(); + $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthiness->isTrue()); } - $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthinessTrinary); + $hasYield = $hasYield || $condResult->hasYield(); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints()); @@ -1336,8 +1481,8 @@ private function processStmtNode( do { $prevScope = $bodyScope; $bodyScope = $bodyScope->mergeWith($initScope); - foreach ($stmt->cond as $condExpr) { - $bodyScope = $this->processExprNode($stmt, $condExpr, $bodyScope, static function (): void { + 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 { @@ -1367,8 +1512,11 @@ private function processStmtNode( } $bodyScope = $bodyScope->mergeWith($initScope); - foreach ($stmt->cond as $condExpr) { - $bodyScope = $this->processExprNode($stmt, $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, $context)->filterOutLoopExitPoints(); @@ -1382,8 +1530,9 @@ private function processStmtNode( $loopScope = $this->processExprNode($stmt, $loopExpr, $loopScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope(); } $finalScope = $finalScope->generalizeWith($loopScope); - foreach ($stmt->cond as $condExpr) { - $finalScope = $finalScope->filterByFalseyValue($condExpr); + + if ($lastCondExpr !== null) { + $finalScope = $finalScope->filterByFalseyValue($lastCondExpr); } foreach ($finalScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { @@ -1409,10 +1558,18 @@ private function processStmtNode( } } + if ($alwaysIterates->yes()) { + $isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0; + } elseif ($isIterableAtLeastOnce->yes()) { + $isAlwaysTerminating = $finalScopeResult->isAlwaysTerminating(); + } else { + $isAlwaysTerminating = false; + } + return new StatementResult( $finalScope, $finalScopeResult->hasYield() || $hasYield, - false/* $finalScopeResult->isAlwaysTerminating() && $isAlwaysIterable*/, + $isAlwaysTerminating, $finalScopeResult->getExitPointsForOuterLoop(), array_merge($throwPoints, $finalScopeResult->getThrowPoints()), array_merge($impurePoints, $finalScopeResult->getImpurePoints()), @@ -1429,9 +1586,11 @@ private function processStmtNode( $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); + $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(); @@ -1440,6 +1599,7 @@ private function processStmtNode( $branchScope = $caseResult->getTruthyScope()->filterByTruthyValue($condExpr); } else { $hasDefaultCase = true; + $fullCondExpr = null; $branchScope = $scopeForBranches; } @@ -1461,8 +1621,9 @@ private function processStmtNode( 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); @@ -1505,7 +1666,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) { @@ -1545,6 +1706,7 @@ private function processStmtNode( } // explicit only + $onlyExplicitIsThrow = true; if (count($matchingThrowPoints) === 0) { foreach ($throwPoints as $throwPointIndex => $throwPoint) { foreach ($catchTypes as $catchTypeIndex => $catchTypeItem) { @@ -1556,13 +1718,20 @@ private function processStmtNode( if (!$throwPoint->isExplicit()) { continue; } + $throwNode = $throwPoint->getNode(); + if ( + !$throwNode instanceof Expr\Throw_ + && !($throwNode instanceof Node\Stmt\Expression && $throwNode->expr instanceof Expr\Throw_) + ) { + $onlyExplicitIsThrow = false; + } $matchingThrowPoints[$throwPointIndex] = $throwPoint; } } } // implicit only - if (count($matchingThrowPoints) === 0) { + if (count($matchingThrowPoints) === 0 || $onlyExplicitIsThrow) { foreach ($throwPoints as $throwPointIndex => $throwPoint) { if ($throwPoint->isExplicit()) { continue; @@ -1593,19 +1762,14 @@ private function processStmtNode( } // emit error - if ($this->detectDeadTypeInMultiCatch) { - foreach ($matchingCatchTypes as $catchTypeIndex => $matched) { - if ($matched) { - continue; - } - $nodeCallback(new CatchWithUnthrownExceptionNode($catchNode, $catchTypes[$catchTypeIndex], $originalCatchTypes[$catchTypeIndex]), $scope); + foreach ($matchingCatchTypes as $catchTypeIndex => $matched) { + if ($matched) { + continue; } + $nodeCallback(new CatchWithUnthrownExceptionNode($catchNode, $catchTypes[$catchTypeIndex], $originalCatchTypes[$catchTypeIndex]), $scope); } if (count($matchingThrowPoints) === 0) { - if (!$this->detectDeadTypeInMultiCatch) { - $nodeCallback(new CatchWithUnthrownExceptionNode($catchNode, $catchType, $originalCatchType), $scope); - } continue; } @@ -1655,7 +1819,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) { @@ -1683,7 +1847,7 @@ 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, $context); $alwaysTerminating = $alwaysTerminating || $finallyResult->isAlwaysTerminating(); @@ -1724,6 +1888,7 @@ private function processStmtNode( $traverser = new NodeTraverser(); $traverser->addVisitor(new class () extends NodeVisitorAbstract { + #[Override] public function leaveNode(Node $node): ?ExistingArrayDimFetch { if (!$node instanceof ArrayDimFetch || $node->dim === null) { @@ -1750,7 +1915,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $nodeCallback($node, $scope); }, ExpressionContext::createDeep(), - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), false, )->getScope(); } elseif ($var instanceof PropertyFetch) { @@ -1800,7 +1965,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { continue; } - $scope = $scope->assignVariable($var->name, new MixedType(), new MixedType()); + $scope = $scope->assignVariable($var->name, new MixedType(), new MixedType(), TrinaryLogic::createYes()); $vars[] = $var->name; } $scope = $this->processVarAnnotation($scope, $vars, $stmt); @@ -1833,7 +1998,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints()); $scope = $scope->exitExpressionAssign($var->var); - $scope = $scope->assignVariable($var->var->name, new MixedType(), new MixedType()); + $scope = $scope->assignVariable($var->var->name, new MixedType(), new MixedType(), TrinaryLogic::createYes()); $vars[] = $var->var->name; } @@ -1849,9 +2014,6 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { if ($const->namespacedName !== null) { $constantName = new Name\FullyQualified($const->namespacedName->toString()); } else { - if ($const->name->toString() === '') { - throw new ShouldNotHappenException('Constant cannot have a empty name'); - } $constantName = new Name\FullyQualified($const->name->toString()); } $scope = $scope->assignExpression(new ConstFetch($constantName), $scope->getType($const->value), $scope->getNativeType($const->value)); @@ -1889,6 +2051,21 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $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) { $hasYield = false; $throwPoints = $overridingThrowPoints ?? []; @@ -1909,6 +2086,57 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { return new StatementResult($scope, $hasYield, false, [], $throwPoints, $impurePoints); } + /** + * @return array{bool, string|null} + */ + private function getDeprecatedAttribute(Scope $scope, Node\Stmt\Function_|Node\Stmt\ClassMethod|Node\PropertyHook $stmt): array + { + $initializerExprContext = InitializerExprContext::fromStubParameter( + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->getFile(), + $stmt, + ); + $isDeprecated = false; + $deprecatedDescription = null; + $deprecatedDescriptionType = null; + foreach ($stmt->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toString() !== 'Deprecated') { + continue; + } + $isDeprecated = true; + $arguments = $attr->args; + foreach ($arguments as $i => $arg) { + $argName = $arg->name; + if ($argName === null) { + if ($i !== 0) { + continue; + } + + $deprecatedDescriptionType = $this->initializerExprTypeResolver->getType($arg->value, $initializerExprContext); + break; + } + + if ($argName->toString() !== 'message') { + continue; + } + + $deprecatedDescriptionType = $this->initializerExprTypeResolver->getType($arg->value, $initializerExprContext); + break; + } + } + } + + if ($deprecatedDescriptionType !== null) { + $constantStrings = $deprecatedDescriptionType->getConstantStrings(); + if (count($constantStrings) === 1) { + $deprecatedDescription = $constantStrings[0]->getValue(); + } + } + + return [$isDeprecated, $deprecatedDescription]; + } + /** * @return ThrowPoint[]|null */ @@ -1984,6 +2212,8 @@ private function createAstClassReflection(Node\Stmt\ClassLike $stmt, string $cla $this->phpDocInheritanceResolver, $this->phpVersion, $this->signatureMapProvider, + $this->deprecationProvider, + $this->attributeReflectionFactory, $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(), @@ -2024,7 +2254,7 @@ private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Clo $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 Array_ || $expr instanceof List_) { + } elseif ($expr instanceof List_) { foreach ($expr->items as $item) { if ($item === null) { continue; @@ -2070,6 +2300,7 @@ private function ensureShallowNonNullability(MutatingScope $scope, Scope $origin $exprToSpecify, $exprTypeWithoutNull, TypeCombinator::removeNull($nativeType), + TrinaryLogic::createYes(), ); return new EnsuredNonNullabilityResult( @@ -2190,6 +2421,7 @@ public function processExprNode(Node\Stmt $stmt, Expr $expr, MutatingScope $scop $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; if ($expr->name instanceof Expr) { return $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); } elseif (in_array($expr->name, Scope::SUPERGLOBAL_VARIABLES, true)) { @@ -2236,13 +2468,14 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); if ($expr instanceof AssignRef) { $scope = $scope->exitExpressionAssign($expr->expr); } - return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); + return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); }, true, ); @@ -2250,6 +2483,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $vars = $this->getAssignedVariables($expr->var); if (count($vars) > 0) { $varChangedScope = false; @@ -2279,6 +2513,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp return new ExpressionResult( $result->getScope()->mergeWith($originalScope), $result->hasYield(), + $result->isAlwaysTerminating(), $result->getThrowPoints(), $result->getImpurePoints(), ); @@ -2292,6 +2527,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $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() @@ -2303,6 +2539,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $functionReflection = null; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; if ($expr->name instanceof Expr) { $nameType = $scope->getType($expr->name); if (!$nameType->isCallable()->no()) { @@ -2318,6 +2555,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $scope = $nameResult->getScope(); $throwPoints = $nameResult->getThrowPoints(); $impurePoints = $nameResult->getImpurePoints(); + $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); if ( $nameType->isObject()->yes() && $nameType->isCallable()->yes() @@ -2333,6 +2571,7 @@ static function (): void { ); $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) { @@ -2367,12 +2606,15 @@ static function (): void { 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 ($functionReflection !== null) { $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $expr, $scope); @@ -2383,6 +2625,13 @@ static function (): void { $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); } + if ( + $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) @@ -2436,7 +2685,7 @@ static function (): void { $functionReflection !== null && in_array($functionReflection->getName(), ['fopen', 'file_get_contents'], true) ) { - $scope = $scope->assignVariable('http_response_header', AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new StringType())), new ArrayType(new IntegerType(), new StringType())); + $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 ( @@ -2454,19 +2703,20 @@ static function (): void { if ( $functionReflection !== null && $functionReflection->getName() === 'array_splice' - && count($expr->getArgs()) >= 1 + && count($expr->getArgs()) >= 2 ) { $arrayArg = $expr->getArgs()[0]->value; $arrayArgType = $scope->getType($arrayArg); - $valueType = $arrayArgType->getIterableValueType(); - if (count($expr->getArgs()) >= 4) { - $replacementType = $scope->getType($expr->getArgs()[3]->value)->toArray(); - $valueType = TypeCombinator::union($valueType, $replacementType->getIterableValueType()); - } + $arrayArgNativeType = $scope->getNativeType($arrayArg); + + $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)->assignExpression( $arrayArg, - new ArrayType($arrayArgType->getIterableKeyType(), $valueType), - new ArrayType($arrayArgType->getIterableKeyType(), $valueType), + $arrayArgType->spliceArray($offsetType, $lengthType, $replacementType), + $arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementType), ); } @@ -2569,6 +2819,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); if (isset($closureCallScope)) { $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); @@ -2615,6 +2866,8 @@ static function (): void { if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr; + $returnType = $parametersAcceptor->getReturnType(); + $isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit(); } $result = $this->processArgs( @@ -2635,7 +2888,7 @@ static function (): void { $nodeCallback(new InvalidateExprNode($expr->var), $scope); $scope = $scope->invalidateExpression($expr->var, true); } - if ($parametersAcceptor !== null) { + if ($parametersAcceptor !== null && !$methodReflection->isStatic()) { $selfOutType = $methodReflection->getSelfOutType(); if ($selfOutType !== null) { $scope = $scope->assignExpression( @@ -2643,7 +2896,7 @@ static function (): void { TemplateTypeHelper::resolveTemplateTypes( $selfOutType, $parametersAcceptor->getResolvedTemplateTypeMap(), - $parametersAcceptor instanceof ParametersAcceptorWithPhpDocs ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createCovariant(), ), $scope->getNativeType($expr->var), @@ -2673,6 +2926,7 @@ static function (): void { $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, $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); @@ -2681,6 +2935,7 @@ static function (): void { return new ExpressionResult( $scope, $exprResult->hasYield(), + false, $exprResult->getThrowPoints(), $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), @@ -2690,6 +2945,7 @@ static function (): void { $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; if ($expr->class instanceof Expr) { $objectClasses = $scope->getType($expr->class)->getObjectClassNames(); if (count($objectClasses) !== 1) { @@ -2706,6 +2962,7 @@ static function (): void { $hasYield = $classResult->hasYield(); $throwPoints = array_merge($throwPoints, $classResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $classResult->getImpurePoints()); + $isAlwaysTerminating = $classResult->isAlwaysTerminating(); foreach ($additionalThrowPoints as $throwPoint) { $throwPoints[] = $throwPoint; } @@ -2801,6 +3058,8 @@ static function (): void { 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(); @@ -2808,18 +3067,15 @@ static function (): void { if ( $methodReflection !== null - && !$methodReflection->isStatic() && ( $methodReflection->hasSideEffects()->yes() - || $methodReflection->getName() === '__construct' + || ( + !$methodReflection->isStatic() + && $methodReflection->getName() === '__construct' + ) ) - && $scopeFunction instanceof MethodReflection - && !$scopeFunction->isStatic() && $scope->isInClass() - && ( - $scope->getClassReflection()->getName() === $methodReflection->getDeclaringClass()->getName() - || $scope->getClassReflection()->isSubclassOf($methodReflection->getDeclaringClass()->getName()) - ) + && $scope->getClassReflection()->is($methodReflection->getDeclaringClass()->getName()) ) { $scope = $scope->invalidateExpression(new Variable('this'), true); } @@ -2831,7 +3087,7 @@ static function (): void { && $scopeFunction instanceof MethodReflection && !$scopeFunction->isStatic() && $scope->isInClass() - && $scope->getClassReflection()->isSubclassOf($methodReflection->getDeclaringClass()->getName()) + && $scope->getClassReflection()->isSubclassOfClass($methodReflection->getDeclaringClass()) ) { $thisType = $scope->getType(new Variable('this')); $methodClassReflection = $methodReflection->getDeclaringClass(); @@ -2847,18 +3103,36 @@ static function (): void { $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } elseif ($expr instanceof PropertyFetch) { + $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($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->getPropertyReflection($propertyHolderType, $propertyName); + if ($propertyReflection !== null && $this->phpVersion->supportsPropertyHooks()) { + $propertyDeclaringClass = $propertyReflection->getDeclaringClass(); + if ($propertyDeclaringClass->hasNativeProperty($propertyName)) { + $nativeProperty = $propertyDeclaringClass->getNativeProperty($propertyName); + $throwPoints = array_merge($throwPoints, $this->getPropertyReadThrowPointsFromGetHook($scopeBeforeVar, $expr, $nativeProperty)); + } + } } } elseif ($expr instanceof Expr\NullsafePropertyFetch) { $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var); @@ -2868,6 +3142,7 @@ static function (): void { return new ExpressionResult( $scope, $exprResult->hasYield(), + false, $exprResult->getThrowPoints(), $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), @@ -2876,12 +3151,22 @@ static function (): void { } elseif ($expr instanceof StaticPropertyFetch) { $hasYield = false; $throwPoints = []; - $impurePoints = []; + $impurePoints = [ + new ImpurePoint( + $scope, + $expr, + 'staticPropertyAccess', + 'static property access', + true, + ), + ]; + $isAlwaysTerminating = false; if ($expr->class instanceof Expr) { $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) { @@ -2889,6 +3174,7 @@ static function (): void { $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) { @@ -2897,6 +3183,7 @@ static function (): void { return new ExpressionResult( $processClosureResult->getScope(), false, + false, [], [], ); @@ -2905,6 +3192,7 @@ static function (): void { return new ExpressionResult( $result->getScope(), $result->hasYield(), + false, [], [], ); @@ -2913,6 +3201,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); } elseif ($expr instanceof Exit_) { $hasYield = false; @@ -2922,6 +3211,7 @@ static function (): void { $impurePoints = [ new ImpurePoint($scope, $expr, $identifier, $identifier, true), ]; + $isAlwaysTerminating = true; if ($expr->expr !== null) { $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); @@ -2929,26 +3219,33 @@ static function (): void { $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) { + 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($stmt, $expr->dim, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); } @@ -2956,23 +3253,23 @@ static function (): void { $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(); } @@ -2980,6 +3277,7 @@ static function (): void { $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); @@ -2998,6 +3296,7 @@ static function (): void { return new ExpressionResult( $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), + $leftResult->isAlwaysTerminating(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr), @@ -3018,6 +3317,7 @@ static function (): void { return new ExpressionResult( $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), + $leftResult->isAlwaysTerminating(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), static fn (): MutatingScope => $leftMergedWithRightScope->filterByTruthyValue($expr), @@ -3042,12 +3342,14 @@ static function (): void { $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($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $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) && @@ -3059,6 +3361,7 @@ static function (): void { $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($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); @@ -3072,11 +3375,13 @@ static function (): void { 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(); @@ -3085,6 +3390,7 @@ static function (): void { $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); @@ -3112,6 +3418,7 @@ static function (): void { $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(); @@ -3122,6 +3429,7 @@ static function (): void { $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) { @@ -3137,6 +3445,7 @@ static function (): void { true, ); $hasYield = true; + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); } elseif ($expr instanceof BooleanNot) { @@ -3145,16 +3454,33 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); } elseif ($expr instanceof Expr\ClassConstFetch) { - $hasYield = false; - $throwPoints = []; - $impurePoints = []; + $isAlwaysTerminating = false; + if ($expr->class instanceof 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(); + } 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); @@ -3164,6 +3490,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->expr); } elseif ($expr instanceof Expr\Isset_) { @@ -3171,6 +3498,7 @@ static function (): void { $throwPoints = []; $impurePoints = []; $nonNullabilityResults = []; + $isAlwaysTerminating = false; foreach ($expr->vars as $var) { $nonNullabilityResult = $this->ensureNonNullability($scope, $var); $scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $var); @@ -3179,6 +3507,7 @@ static function (): void { $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $nonNullabilityResults[] = $nonNullabilityResult; } foreach (array_reverse($expr->vars) as $var) { @@ -3193,22 +3522,25 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); if ($expr->class instanceof Expr) { $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 = []; $impurePoints = []; + $isAlwaysTerminating = false; $className = null; if ($expr->class instanceof Expr || $expr->class instanceof Name) { if ($expr->class instanceof Expr) { @@ -3227,6 +3559,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); foreach ($additionalThrowPoints as $throwPoint) { $throwPoints[] = $throwPoint; } @@ -3322,6 +3655,7 @@ static function (): void { $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 @@ -3333,6 +3667,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $newExpr = $expr; if ($expr instanceof Expr\PostInc) { @@ -3354,13 +3689,14 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $nodeCallback($node, $scope); }, $context, - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), false, )->getScope(); } elseif ($expr instanceof Ternary) { $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; @@ -3399,6 +3735,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { return new ExpressionResult( $finalScope, $ternaryCondResult->hasYield(), + $isAlwaysTerminating, $throwPoints, $impurePoints, static fn (): MutatingScope => $finalScope->filterByTruthyValue($expr), @@ -3418,17 +3755,20 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { true, ), ]; + $isAlwaysTerminating = false; if ($expr->key !== null) { $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($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_) { @@ -3439,6 +3779,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); $impurePoints = $condResult->getImpurePoints(); + $isAlwaysTerminating = $condResult->isAlwaysTerminating(); $matchScope = $scope->enterMatch($expr); $armNodes = []; $hasDefaultCond = false; @@ -3463,6 +3804,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $condNodes = []; $conditionCases = []; + $conditionExprs = []; foreach ($arm->conds as $cond) { if (!$cond instanceof Expr\ClassConstFetch) { continue 2; @@ -3520,6 +3862,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $armConditionScope, $cond->getStartLine(), ); + $conditionExprs[] = $cond; unset($unusedIndexedEnumCases[$loweredFetchedClassName][$caseName]); } @@ -3533,10 +3876,11 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $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()); @@ -3612,22 +3956,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $filteringExprs[] = $armCond; } - if (count($filteringExprs) === 1) { - $filteringExpr = new BinaryOp\Identical($expr->cond, $filteringExprs[0]); - } else { - $items = []; - foreach ($filteringExprs as $filteringExpr) { - $items[] = new ArrayItem($filteringExpr); - } - $filteringExpr = new FuncCall( - new Name\FullyQualified('in_array'), - [ - new Arg($expr->cond), - new Arg(new Array_($items)), - new Arg(new ConstFetch(new Name\FullyQualified('true'))), - ], - ); - } + $filteringExpr = $this->getFilteringExprForMatchArm($expr, $filteringExprs); $bodyScope = $this->processExprNode($stmt, $filteringExpr, $matchScope, static function (): void { }, $deepContext)->getTruthyScope(); @@ -3662,23 +3991,27 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $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($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()); @@ -3686,23 +4019,27 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $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()); @@ -3710,36 +4047,43 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $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, $impurePoints, static fn (): MutatingScope => $scope->filterByTruthyValue($expr), @@ -3822,7 +4166,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra ? TypeCombinator::intersect($array, new NonEmptyArrayType()) : $array; $constantArray = $isList - ? AccessoryArrayListType::intersectWith($constantArray) + ? TypeCombinator::intersect($constantArray, new AccessoryArrayListType()) : $constantArray; } @@ -3861,11 +4205,11 @@ private function getArraySortPreserveListFunctionType(Type $type): Type return $traverse($type); } - if (!$type instanceof ArrayType) { + if (!$type instanceof ArrayType && !$type instanceof ConstantArrayType) { return $type; } - $newArrayType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $type->getIterableValueType())); + $newArrayType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $type->getIterableValueType()), new AccessoryArrayListType()); if ($isIterableAtLeastOnce->yes()) { $newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType()); } @@ -4046,7 +4390,7 @@ private function getConstructorThrowPoint(MethodReflection $constructorReflectio return ThrowPoint::createExplicit($scope, $throwType, $new, true); } } elseif ($this->implicitThrows) { - if ($classReflection->getName() !== Throwable::class && !$classReflection->isSubclassOf(Throwable::class)) { + if (!$classReflection->is(Throwable::class)) { return ThrowPoint::createImplicit($scope, $methodCall); } } @@ -4087,6 +4431,83 @@ private function getStaticMethodThrowPoint(MethodReflection $methodReflection, P return null; } + /** + * @return ThrowPoint[] + */ + private function getPropertyReadThrowPointsFromGetHook( + MutatingScope $scope, + PropertyFetch $propertyFetch, + PhpPropertyReflection $propertyReflection, + ): array + { + return $this->getThrowPointsFromPropertyHook($scope, $propertyFetch, $propertyReflection, 'get'); + } + + /** + * @return ThrowPoint[] + */ + private function getPropertyAssignThrowPointsFromSetHook( + MutatingScope $scope, + PropertyFetch $propertyFetch, + PhpPropertyReflection $propertyReflection, + ): array + { + return $this->getThrowPointsFromPropertyHook($scope, $propertyFetch, $propertyReflection, 'set'); + } + + /** + * @param 'get'|'set' $hookName + * @return ThrowPoint[] + */ + private function getThrowPointsFromPropertyHook( + MutatingScope $scope, + PropertyFetch $propertyFetch, + PhpPropertyReflection $propertyReflection, + string $hookName, + ): array + { + $scopeFunction = $scope->getFunction(); + if ( + $scopeFunction instanceof PhpMethodFromParserNodeReflection + && $scopeFunction->isPropertyHook() + && $propertyFetch->var instanceof Variable + && $propertyFetch->var->name === 'this' + && $propertyFetch->name instanceof Identifier + && $propertyFetch->name->toString() === $scopeFunction->getHookedPropertyName() + ) { + return []; + } + $declaringClass = $propertyReflection->getDeclaringClass(); + if (!$propertyReflection->hasHook($hookName)) { + if ( + $propertyReflection->isPrivate() + || $propertyReflection->isFinal()->yes() + || $declaringClass->isFinal() + ) { + return []; + } + + if ($this->implicitThrows) { + return [ThrowPoint::createImplicit($scope, $propertyFetch)]; + } + + return []; + } + + $getHook = $propertyReflection->getHook($hookName); + $throwType = $getHook->getThrowType(); + + if ($throwType !== null) { + if (!$throwType->isVoid()->yes()) { + return [ThrowPoint::createExplicit($scope, $throwType, $propertyFetch, true)]; + } + } elseif ($this->implicitThrows) { + return [ThrowPoint::createImplicit($scope, $propertyFetch)]; + } + + return []; + } + /** * @return string[] */ @@ -4100,7 +4521,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) { @@ -4196,7 +4617,7 @@ private function processClosureNode( $variableNativeType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideNativeType); } } - $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType, $variableNativeType); + $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType, $variableNativeType, TrinaryLogic::createYes()); } } $this->processExprNode($stmt, $use->var, $useScope, $nodeCallback, $context); @@ -4218,6 +4639,9 @@ private function processClosureNode( throw new ShouldNotHappenException(); } + $returnType = $closureType->getReturnType(); + $isAlwaysTerminating = ($returnType instanceof NeverType && $returnType->isExplicit()); + $nodeCallback(new InClosureNode($closureType, $expr), $closureScope); $executionEnds = []; @@ -4257,6 +4681,7 @@ private function processClosureNode( $gatheredReturnStatements[] = new ReturnStatement($scope, $node); }; + if (count($byRefUses) === 0) { $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel()); $nodeCallback(new ClosureReturnStatementsNode( @@ -4268,10 +4693,11 @@ private function processClosureNode( array_merge($statementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope); - return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions); + return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $isAlwaysTerminating); } $count = 0; + $closureResultScope = null; do { $prevScope = $closureScope; @@ -4281,8 +4707,15 @@ private function processClosureNode( 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; } @@ -4292,6 +4725,10 @@ private function processClosureNode( $count++; } while ($count < self::LOOP_SCOPE_ITERATIONS); + if ($closureResultScope === null) { + $closureResultScope = $closureScope; + } + $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel()); $nodeCallback(new ClosureReturnStatementsNode( $expr, @@ -4302,7 +4739,7 @@ private function processClosureNode( array_merge($statementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope); - return new ProcessClosureResult($scope->processClosureScope($closureScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions); + return new ProcessClosureResult($scope->processClosureScope($closureResultScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $isAlwaysTerminating); } /** @@ -4370,7 +4807,7 @@ private function processArrowFunctionNode( $nodeCallback(new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope); $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel()); - return new ExpressionResult($scope, false, $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); + return new ExpressionResult($scope, false, $exprResult->isAlwaysTerminating(), $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); } /** @@ -4504,6 +4941,125 @@ private function processAttributeGroups( } } + /** + * @param Node\PropertyHook[] $hooks + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processPropertyHooks( + Node\Stmt $stmt, + Identifier|Name|ComplexType|null $nativeTypeNode, + ?Type $phpDocType, + string $propertyName, + array $hooks, + MutatingScope $scope, + callable $nodeCallback, + ): void + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + $classReflection = $scope->getClassReflection(); + + foreach ($hooks as $hook) { + $nodeCallback($hook, $scope); + $this->processAttributeGroups($stmt, $hook->attrGroups, $scope, $nodeCallback); + + [, $phpDocParameterTypes,,,, $phpDocThrowType,,,,,,,, $phpDocComment] = $this->getPhpDocs($scope, $hook); + + foreach ($hook->params as $param) { + $this->processParamNode($stmt, $param, $scope, $nodeCallback); + } + + [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $hook); + + $hookScope = $scope->enterPropertyHook( + $hook, + $propertyName, + $nativeTypeNode, + $phpDocType, + $phpDocParameterTypes, + $phpDocThrowType, + $deprecatedDescription, + $isDeprecated, + $phpDocComment, + ); + $hookReflection = $hookScope->getFunction(); + if (!$hookReflection instanceof PhpMethodFromParserNodeReflection) { + throw new ShouldNotHappenException(); + } + + if (!$classReflection->hasNativeProperty($propertyName)) { + throw new ShouldNotHappenException(); + } + + $propertyReflection = $classReflection->getNativeProperty($propertyName); + + $nodeCallback(new InPropertyHookNode( + $classReflection, + $hookReflection, + $propertyReflection, + $hook, + ), $hookScope); + + $stmts = $hook->getStmts(); + if ($stmts === null) { + return; + } + + if ($hook->body instanceof Expr) { + // enrich attributes of nodes in short hook body statements + $traverser = new NodeTraverser( + new LineAttributesVisitor($hook->body->getStartLine(), $hook->body->getEndLine()), + ); + $traverser->traverse($stmts); + } + + $gatheredReturnStatements = []; + $executionEnds = []; + $methodImpurePoints = []; + $statementResult = $this->processStmtNodes(new PropertyHookStatementNode($hook), $stmts, $hookScope, static function (Node $node, Scope $scope) use ($nodeCallback, $hookScope, &$gatheredReturnStatements, &$executionEnds, &$hookImpurePoints): void { + $nodeCallback($node, $scope); + if ($scope->getFunction() !== $hookScope->getFunction()) { + return; + } + if ($scope->isInAnonymousFunction()) { + return; + } + if ($node instanceof PropertyAssignNode) { + $hookImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } + if ($node instanceof ExecutionEndNode) { + $executionEnds[] = $node; + return; + } + if (!$node instanceof Return_) { + return; + } + + $gatheredReturnStatements[] = new ReturnStatement($scope, $node); + }, StatementContext::createTopLevel()); + + $nodeCallback(new PropertyHookReturnStatementsNode( + $hook, + $gatheredReturnStatements, + $statementResult, + $executionEnds, + array_merge($statementResult->getImpurePoints(), $methodImpurePoints), + $classReflection, + $hookReflection, + $propertyReflection, + ), $hookScope); + } + } + /** * @param MethodReflection|FunctionReflection|null $calleeReflection * @param callable(Node $node, Scope $scope): void $nodeCallback @@ -4529,6 +5085,7 @@ private function processArgs( $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; foreach ($args as $i => $arg) { $assignByReference = false; $parameter = null; @@ -4539,7 +5096,7 @@ private function processArgs( $assignByReference = $parameters[$i]->passedByReference()->createsNewVariable(); $parameterType = $parameters[$i]->getType(); - if ($parameters[$i] instanceof ParameterReflectionWithPhpDocs) { + if ($parameters[$i] instanceof ExtendedParameterReflection) { $parameterNativeType = $parameters[$i]->getNativeType(); } $parameter = $parameters[$i]; @@ -4548,7 +5105,7 @@ private function processArgs( $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); $parameterType = $lastParameter->getType(); - if ($lastParameter instanceof ParameterReflectionWithPhpDocs) { + if ($lastParameter instanceof ExtendedParameterReflection) { $parameterNativeType = $lastParameter->getNativeType(); } $parameter = $lastParameter; @@ -4557,20 +5114,18 @@ private function processArgs( $lookForUnset = false; if ($assignByReference) { - if ($arg->value instanceof Variable) { - $isBuiltin = false; - if ($calleeReflection instanceof FunctionReflection && $calleeReflection->isBuiltin()) { - $isBuiltin = true; - } elseif ($calleeReflection instanceof ExtendedMethodReflection && $calleeReflection->getDeclaringClass()->isBuiltin()) { - $isBuiltin = true; - } - if ( - $isBuiltin - || ($parameterNativeType === null || !$parameterNativeType->isNull()->no()) - ) { - $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $arg->value); - $lookForUnset = true; - } + $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; } } @@ -4587,26 +5142,32 @@ private function processArgs( $scopeToPass = $closureBindScope; } - if ($parameter instanceof ParameterReflectionWithPhpDocs) { + $parameterCallableType = null; + if ($parameterType !== null) { + $parameterCallableType = TypeUtils::findCallableType($parameterType); + } + + if ($parameter instanceof ExtendedParameterReflection) { $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable(); if ($parameterCallImmediately->maybe()) { - $callCallbackImmediately = $calleeReflection instanceof FunctionReflection; + $callCallbackImmediately = $parameterCallableType !== null && $calleeReflection instanceof FunctionReflection; } else { $callCallbackImmediately = $parameterCallImmediately->yes(); } } else { - $callCallbackImmediately = $calleeReflection instanceof FunctionReflection; + $callCallbackImmediately = $parameterCallableType !== null && $calleeReflection instanceof FunctionReflection; } + if ($arg->value instanceof Expr\Closure) { $restoreThisScope = null; if ( $closureBindScope === null - && $parameter instanceof ParameterReflectionWithPhpDocs + && $parameter instanceof ExtendedParameterReflection && $parameter->getClosureThisType() !== null && !$arg->value->static ) { $restoreThisScope = $scopeToPass; - $scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType()); + $scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType(), TrinaryLogic::createYes()); } if ($parameter !== null) { @@ -4622,6 +5183,7 @@ private function processArgs( 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 = []; @@ -4654,11 +5216,11 @@ private function processArgs( } elseif ($arg->value instanceof Expr\ArrowFunction) { if ( $closureBindScope === null - && $parameter instanceof ParameterReflectionWithPhpDocs + && $parameter instanceof ExtendedParameterReflection && $parameter->getClosureThisType() !== null && !$arg->value->static ) { - $scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType()); + $scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType(), TrinaryLogic::createYes()); } if ($parameter !== null) { @@ -4674,12 +5236,14 @@ private function processArgs( 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 { $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(); @@ -4694,6 +5258,8 @@ private function processArgs( } $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()); } } } @@ -4730,18 +5296,16 @@ private function processArgs( if ($currentParameter !== null) { $assignByReference = $currentParameter->passedByReference()->createsNewVariable(); if ($assignByReference) { - if ($currentParameter instanceof ParameterReflectionWithPhpDocs && $currentParameter->getOutType() !== null) { + if ($currentParameter instanceof ExtendedParameterReflection && $currentParameter->getOutType() !== null) { $byRefType = $currentParameter->getOutType(); } elseif ( $calleeReflection instanceof MethodReflection && !$calleeReflection->getDeclaringClass()->isBuiltin() - && $this->paramOutType ) { $byRefType = $currentParameter->getType(); } elseif ( $calleeReflection instanceof FunctionReflection && !$calleeReflection->isBuiltin() - && $this->paramOutType ) { $byRefType = $currentParameter->getType(); } @@ -4754,18 +5318,29 @@ private function processArgs( } $argValue = $arg->value; - if ($argValue instanceof Variable && is_string($argValue->name)) { - if ($argValue->name !== 'this') { - $paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope); - if ($paramOutType !== null) { - $byRefType = $paramOutType; - } - - $nodeCallback(new VariableAssignNode($argValue, new TypeExpr($byRefType), false), $scope); - $scope = $scope->assignVariable($argValue->name, $byRefType, new MixedType()); + if (!$argValue instanceof Variable || $argValue->name !== 'this') { + $paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope); + if ($paramOutType !== null) { + $byRefType = $paramOutType; } - } else { - $scope = $scope->invalidateExpression($argValue); + + $result = $this->processAssignVar( + $scope, + $stmt, + $argValue, + new TypeExpr($byRefType), + static function (Node $node, Scope $scope) use ($nodeCallback): void { + if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { + return; + } + + $nodeCallback($node, $scope); + }, + $context, + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), + true, + ); + $scope = $result->getScope(); } } elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) { $argType = $scope->getType($arg->value); @@ -4795,7 +5370,7 @@ private function processArgs( } } - return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); + return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); } /** @@ -4902,12 +5477,14 @@ private function processAssignVar( $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); } @@ -4940,6 +5517,8 @@ private function processAssignVar( } } + $scopeBeforeAssignEval = $scope; + $scope = $result->getScope(); $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy()); $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createFalsey()); @@ -4951,8 +5530,8 @@ private function processAssignVar( $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); - $nodeCallback(new VariableAssignNode($var, $assignedExpr, $isAssignOp), $result->getScope()); - $scope = $result->getScope()->assignVariable($var->name, $type, $scope->getNativeType($assignedExpr)); + $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); } @@ -4961,9 +5540,17 @@ private function processAssignVar( $originalVar = $var; $assignedPropertyExpr = $assignedExpr; while ($var instanceof ArrayDimFetch) { - $varForSetOffsetValue = $var->var; - if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) { - $varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue); + if ( + $var->var instanceof PropertyFetch + || $var->var instanceof StaticPropertyFetch + ) { + if (((new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($var->var))->yes())) { + $varForSetOffsetValue = $var->var; + } else { + $varForSetOffsetValue = new OriginalPropertyTypeExpr($var->var); + } + } else { + $varForSetOffsetValue = $var->var; } $assignedPropertyExpr = new SetOffsetValueTypeExpr( $varForSetOffsetValue, @@ -4982,6 +5569,7 @@ private function processAssignVar( $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); if ($enterExpressionAssign) { $scope = $scope->exitExpressionAssign($var); @@ -5025,13 +5613,15 @@ private function processAssignVar( $valueToWrite = $scope->getType($assignedExpr); $nativeValueToWrite = $scope->getNativeType($assignedExpr); $originalValueToWrite = $valueToWrite; - $originalNativeValueToWrite = $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); @@ -5046,75 +5636,46 @@ private function processAssignVar( } $offsetValueType = $varType; $offsetNativeValueType = $varNativeType; - $offsetValueTypeStack = [$offsetValueType]; - $offsetValueNativeTypeStack = [$offsetNativeValueType]; - 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_slice($offsetNativeTypes, 0, -1) as $offsetNativeType) { - if ($offsetNativeType === null) { - $offsetNativeValueType = new ConstantArrayType([], []); + $valueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope); - } else { - $offsetNativeValueType = $offsetNativeValueType->getOffsetValueType($offsetNativeType); - if ($offsetNativeValueType instanceof ErrorType) { - $offsetNativeValueType = new ConstantArrayType([], []); - } - } + if (!$offsetValueType->equals($offsetNativeValueType) || !$valueToWrite->equals($nativeValueToWrite)) { + $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); + } else { + $rewritten = false; + foreach ($offsetTypes as $i => $offsetType) { + $offsetNativeType = $offsetNativeTypes[$i]; - $offsetValueNativeTypeStack[] = $offsetNativeValueType; - } + if ($offsetType === null) { + if ($offsetNativeType !== null) { + throw new ShouldNotHappenException(); + } - foreach (array_reverse($offsetTypes) as $i => $offsetType) { - /** @var Type $offsetValueType */ - $offsetValueType = array_pop($offsetValueTypeStack); - if (!$offsetValueType instanceof MixedType) { - $types = [ - new ArrayType(new MixedType(), new MixedType()), - new ObjectType(ArrayAccess::class), - new NullType(), - ]; - if ($offsetType !== null && $offsetType->isInteger()->yes()) { - $types[] = new StringType(); + continue; + } elseif ($offsetNativeType === null) { + throw new ShouldNotHappenException(); } - $offsetValueType = TypeCombinator::intersect($offsetValueType, TypeCombinator::union(...$types)); - } - $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); - } - foreach (array_reverse($offsetNativeTypes) as $i => $offsetNativeType) { - /** @var Type $offsetNativeValueType */ - $offsetNativeValueType = array_pop($offsetValueNativeTypeStack); - if (!$offsetNativeValueType instanceof MixedType) { - $types = [ - new ArrayType(new MixedType(), new MixedType()), - new ObjectType(ArrayAccess::class), - new NullType(), - ]; - if ($offsetNativeType !== null && $offsetNativeType->isInteger()->yes()) { - $types[] = new StringType(); + if ($offsetType->equals($offsetNativeType)) { + continue; } - $offsetNativeValueType = TypeCombinator::intersect($offsetNativeValueType, TypeCombinator::union(...$types)); + + $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); + $rewritten = true; + break; + } + + if (!$rewritten) { + $nativeValueToWrite = $valueToWrite; } - $nativeValueToWrite = $offsetNativeValueType->setOffsetValueType($offsetNativeType, $nativeValueToWrite, $i === 0); } if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { if ($var instanceof Variable && is_string($var->name)) { - $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); - $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite); + $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), $scope); + $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()); } @@ -5128,7 +5689,11 @@ private function processAssignVar( if ($originalVar->dim instanceof Variable || $originalVar->dim instanceof Node\Scalar) { $currentVarType = $scope->getType($originalVar); - if (!$originalValueToWrite->isSuperTypeOf($currentVarType)->yes()) { + $currentVarNativeType = $scope->getNativeType($originalVar); + if ( + !$originalValueToWrite->isSuperTypeOf($currentVarType)->yes() + || !$originalNativeValueToWrite->isSuperTypeOf($currentVarNativeType)->yes() + ) { $scope = $scope->assignExpression( $originalVar, $originalValueToWrite, @@ -5138,9 +5703,9 @@ private function processAssignVar( } } else { if ($var instanceof Variable) { - $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr), $scopeBeforeAssignEval); } elseif ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { - $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + $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()); } @@ -5162,6 +5727,7 @@ static function (): void { $hasYield = $objectResult->hasYield(); $throwPoints = $objectResult->getThrowPoints(); $impurePoints = $objectResult->getImpurePoints(); + $isAlwaysTerminating = $objectResult->isAlwaysTerminating(); $scope = $objectResult->getScope(); $propertyName = null; @@ -5172,22 +5738,53 @@ static function (): void { $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->hasProperty($propertyName)->yes()) { $propertyReflection = $propertyHolderType->getProperty($propertyName, $scope); $assignedExprType = $scope->getType($assignedExpr); - $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); + $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval); if ($propertyReflection->canChangeTypeAfterAssignment()) { - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + if ($propertyReflection->hasNativeType()) { + $propertyNativeType = $propertyReflection->getNativeType(); + + $assignedTypeIsCompatible = $propertyNativeType->isSuperTypeOf($assignedExprType)->yes(); + if (!$assignedTypeIsCompatible) { + foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) { + if ($type->isSuperTypeOf($assignedExprType)->yes()) { + $assignedTypeIsCompatible = true; + break; + } + } + } + + if ($assignedTypeIsCompatible) { + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + } else { + $scope = $scope->assignExpression( + $var, + TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), + TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), + ); + } + } else { + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + } } $declaringClass = $propertyReflection->getDeclaringClass(); if ($declaringClass->hasNativeProperty($propertyName)) { @@ -5197,6 +5794,9 @@ static function (): void { ) { $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); } @@ -5204,7 +5804,7 @@ static function (): void { } else { // fallback $assignedExprType = $scope->getType($assignedExpr); - $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); + $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()) { @@ -5235,33 +5835,61 @@ static function (): void { $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->getPropertyReflection($propertyHolderType, $propertyName); $assignedExprType = $scope->getType($assignedExpr); - $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); + $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval); if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) { - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + if ($propertyReflection->hasNativeType()) { + $propertyNativeType = $propertyReflection->getNativeType(); + $assignedTypeIsCompatible = $propertyNativeType->isSuperTypeOf($assignedExprType)->yes(); + + if (!$assignedTypeIsCompatible) { + foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) { + if ($type->isSuperTypeOf($assignedExprType)->yes()) { + $assignedTypeIsCompatible = true; + break; + } + } + } + + if ($assignedTypeIsCompatible) { + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + } else { + $scope = $scope->assignExpression( + $var, + TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), + TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), + ); + } + } else { + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + } } } else { // fallback $assignedExprType = $scope->getType($assignedExpr); - $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); + $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval); $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); } - } elseif ($var instanceof List_ || $var instanceof Array_) { + } elseif ($var instanceof List_) { $result = $processExprCallback($scope); $hasYield = $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); foreach ($var->items as $i => $arrayItem) { if ($arrayItem === null) { @@ -5279,6 +5907,7 @@ static function (): void { $hasYield = $hasYield || $keyResult->hasYield(); $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $keyResult->isAlwaysTerminating(); $itemScope = $keyResult->getScope(); } @@ -5286,9 +5915,10 @@ static function (): void { $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\LNumber($i); + $dimExpr = new Node\Scalar\Int_($i); } else { $dimExpr = $arrayItem->key; } @@ -5299,21 +5929,30 @@ static function (): void { new GetOffsetValueTypeExpr($assignedExpr, $dimExpr), $nodeCallback, $context, - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), + 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) { - $varForSetOffsetValue = $var->getVar(); - if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) { - $varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue); + 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, @@ -5362,8 +6001,8 @@ static function (): void { } if ($var instanceof Variable && is_string($var->name)) { - $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); - $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite); + $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); @@ -5376,7 +6015,104 @@ static function (): void { } } - return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); + return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + } + + /** + * @param list $dimFetchStack + * @param list $offsetTypes + */ + private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, Scope $scope): Type + { + $offsetValueTypeStack = [$offsetValueType]; + foreach (array_slice($offsetTypes, 0, -1) as $offsetType) { + if ($offsetType === null) { + $offsetValueType = new ConstantArrayType([], []); + + } else { + $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); + if ($offsetValueType instanceof ErrorType) { + $offsetValueType = new ConstantArrayType([], []); + } + } + + $offsetValueTypeStack[] = $offsetValueType; + } + + foreach (array_reverse($offsetTypes) as $i => $offsetType) { + /** @var Type $offsetValueType */ + $offsetValueType = array_pop($offsetValueTypeStack); + if ( + !$offsetValueType instanceof MixedType + && !$offsetValueType->isConstantArray()->yes() + ) { + $types = [ + new ArrayType(new MixedType(), new MixedType()), + new ObjectType(ArrayAccess::class), + new NullType(), + ]; + if ($offsetType !== null && $offsetType->isInteger()->yes()) { + $types[] = new StringType(); + } + $offsetValueType = TypeCombinator::intersect($offsetValueType, TypeCombinator::union(...$types)); + } + + $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(), + ); + } + } + + } else { + $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); + } + + if ($arrayDimFetch === null || !$offsetValueType->isList()->yes()) { + continue; + } + + if ($scope->hasExpressionType($arrayDimFetch)->yes()) { // keep list for $list[$index] assignments + $valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); + } elseif ($arrayDimFetch->dim instanceof BinaryOp\Plus) { + if ( // keep list for $list[$index + 1] assignments + $arrayDimFetch->dim->right instanceof Variable + && $arrayDimFetch->dim->left instanceof Node\Scalar\Int_ + && $arrayDimFetch->dim->left->value === 1 + && $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->right))->yes() + ) { + $valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); + } elseif ( // keep list for $list[1 + $index] assignments + $arrayDimFetch->dim->left instanceof Variable + && $arrayDimFetch->dim->right instanceof Node\Scalar\Int_ + && $arrayDimFetch->dim->right->value === 1 + && $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->left))->yes() + ) { + $valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); + } + } + } + + return $valueToWrite; } private function unwrapAssign(Expr $expr): Expr @@ -5572,13 +6308,13 @@ private function processVarAnnotation(MutatingScope $scope, array $variableNames $variableType = $varTags[$variableName]->getType(); $changed = true; - $scope = $scope->assignVariable($variableName, $variableType, new MixedType()); + $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, new MixedType()); + $scope = $scope->assignVariable($variableNames[0], $variableType, new MixedType(), TrinaryLogic::createYes()); } return $scope; @@ -5589,6 +6325,7 @@ private function enterForeach(MutatingScope $scope, MutatingScope $originalScope if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); } + $iterateeType = $originalScope->getType($stmt->expr); if ( ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)) @@ -5617,7 +6354,7 @@ private function enterForeach(MutatingScope $scope, MutatingScope $originalScope static function (): void { }, ExpressionContext::createDeep(), - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), true, )->getScope(); $vars = $this->getAssignedVariables($stmt->valueVar); @@ -5635,7 +6372,7 @@ static function (): void { static function (): void { }, ExpressionContext::createDeep(), - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), true, )->getScope(); $vars = array_merge($vars, $this->getAssignedVariables($stmt->keyVar)); @@ -5764,7 +6501,7 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection $methodAst = clone $stmt; $stmts[$i] = $methodAst; if (array_key_exists($methodName, $methodModifiers)) { - $methodAst->flags = ($methodAst->flags & ~ Node\Stmt\Class_::VISIBILITY_MODIFIER_MASK) | $methodModifiers[$methodName]; + $methodAst->flags = ($methodAst->flags & ~ Modifiers::VISIBILITY_MASK) | $methodModifiers[$methodName]; } if (!array_key_exists($methodName, $methodNames)) { @@ -5775,8 +6512,11 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection $methodAst->name = $methodNames[$methodName]; } + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } $traitScope = $scope->enterTrait($traitReflection); - $nodeCallback(new InTraitNode($node, $traitReflection), $traitScope); + $nodeCallback(new InTraitNode($node, $traitReflection, $scope->getClassReflection()), $traitScope); $this->processStmtNodes($node, $stmts, $traitScope, $nodeCallback, StatementContext::createTopLevel()); return; } @@ -5850,9 +6590,6 @@ private function processCalledMethod(MethodReflection $methodReflection): ?Mutat foreach ($returnStatement->getExecutionEnds() as $executionEnd) { $statementResult = $executionEnd->getStatementResult(); $endNode = $executionEnd->getNode(); - if ($endNode instanceof Node\Stmt\Throw_) { - continue; - } if ($endNode instanceof Node\Stmt\Expression) { $exprType = $statementResult->getScope()->getType($endNode->expr); if ($exprType instanceof NeverType && $exprType->isExplicit()) { @@ -5893,7 +6630,7 @@ private function processNodesForCalledMethod($node, string $fileName, MethodRefl $declaringClass = $methodReflection->getDeclaringClass(); if ( $node instanceof Node\Stmt\Class_ - && $node->namespacedName !== null + && isset($node->namespacedName) && $declaringClass->getName() === (string) $node->namespacedName && $declaringClass->getNativeReflection()->getStartLine() === $node->getStartLine() ) { @@ -6025,6 +6762,11 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n } } 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) { @@ -6097,7 +6839,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); @@ -6129,22 +6871,55 @@ private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $ } /** - * @template T of Node - * @param array $nodes - * @return T|null + * @param array $nodes + * @return list */ - private function getFirstUnreachableNode(array $nodes, bool $earlyBinding): ?Node + private function getNextUnreachableStatements(array $nodes, bool $earlyBinding): array { + $stmts = []; + $isPassedUnreachableStatement = false; foreach ($nodes as $node) { - if ($node instanceof Node\Stmt\Nop) { + if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) { continue; } - if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) { + if ($isPassedUnreachableStatement && $node instanceof Node\Stmt) { + $stmts[] = $node; + continue; + } + if ($node instanceof Node\Stmt\Nop || $node instanceof Node\Stmt\InlineHTML) { continue; } - return $node; + if (!$node instanceof Node\Stmt) { + continue; + } + $stmts[] = $node; + $isPassedUnreachableStatement = true; } - return null; + return $stmts; + } + + /** + * @param array $conditions + */ + public function getFilteringExprForMatchArm(Expr\Match_ $expr, array $conditions): BinaryOp\Identical|FuncCall + { + if (count($conditions) === 1) { + return new BinaryOp\Identical($expr->cond, $conditions[0]); + } + + $items = []; + foreach ($conditions as $filteringExpr) { + $items[] = new Node\ArrayItem($filteringExpr); + } + + return new FuncCall( + new Name\FullyQualified('in_array'), + [ + new Arg($expr->cond), + new Arg(new Array_($items)), + new Arg(new ConstFetch(new Name\FullyQualified('true'))), + ], + ); } } diff --git a/src/Analyser/NullsafeOperatorHelper.php b/src/Analyser/NullsafeOperatorHelper.php index 9b34346264..0b439191c7 100644 --- a/src/Analyser/NullsafeOperatorHelper.php +++ b/src/Analyser/NullsafeOperatorHelper.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr; use PHPStan\Type\TypeCombinator; -class NullsafeOperatorHelper +final class NullsafeOperatorHelper { public static function getNullsafeShortcircuitedExprRespectingScope(Scope $scope, Expr $expr): Expr diff --git a/src/Analyser/OutOfClassScope.php b/src/Analyser/OutOfClassScope.php index d9ddf23f2f..a2215bc25c 100644 --- a/src/Analyser/OutOfClassScope.php +++ b/src/Analyser/OutOfClassScope.php @@ -2,13 +2,14 @@ namespace PHPStan\Analyser; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\ConstantReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\PropertyReflection; -class OutOfClassScope implements ClassMemberAccessAnswerer +final class OutOfClassScope implements ClassMemberAccessAnswerer { /** @api */ @@ -31,12 +32,24 @@ public function canAccessProperty(PropertyReflection $propertyReflection): bool return $propertyReflection->isPublic(); } + public function canReadProperty(ExtendedPropertyReflection $propertyReflection): bool + { + return $propertyReflection->isPublic(); + } + + public function canWriteProperty(ExtendedPropertyReflection $propertyReflection): bool + { + return $propertyReflection->isPublic() + && !$propertyReflection->isProtectedSet() + && !$propertyReflection->isPrivateSet(); + } + public function canCallMethod(MethodReflection $methodReflection): bool { return $methodReflection->isPublic(); } - public function canAccessConstant(ConstantReflection $constantReflection): bool + public function canAccessConstant(ClassConstantReflection $constantReflection): bool { return $constantReflection->isPublic(); } diff --git a/src/Analyser/ProcessClosureResult.php b/src/Analyser/ProcessClosureResult.php index 7423ac220d..a133bfa77b 100644 --- a/src/Analyser/ProcessClosureResult.php +++ b/src/Analyser/ProcessClosureResult.php @@ -4,7 +4,7 @@ use PHPStan\Node\InvalidateExprNode; -class ProcessClosureResult +final class ProcessClosureResult { /** @@ -17,6 +17,7 @@ public function __construct( private array $throwPoints, private array $impurePoints, private array $invalidateExpressions, + private bool $isAlwaysTerminating, ) { } @@ -50,4 +51,9 @@ 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 8b592f68fe..5cfefc6f46 100644 --- a/src/Analyser/ResultCache/ResultCache.php +++ b/src/Analyser/ResultCache/ResultCache.php @@ -9,8 +9,9 @@ /** * @phpstan-import-type LinesToIgnore from FileAnalyserResult + * @phpstan-import-type CollectorData from CollectedData */ -class ResultCache +final class ResultCache { /** @@ -20,10 +21,12 @@ class ResultCache * @param array> $locallyIgnoredErrors * @param array $linesToIgnore * @param array $unmatchedLineIgnores - * @param array> $collectedData + * @param CollectorData $collectedData * @param array> $dependencies + * @param array> $usedTraitDependencies * @param array> $exportedNodes * @param array $projectExtensionFiles + * @param array $currentFileHashes */ public function __construct( private array $filesToAnalyse, @@ -36,8 +39,10 @@ public function __construct( private array $unmatchedLineIgnores, private array $collectedData, private array $dependencies, + private array $usedTraitDependencies, private array $exportedNodes, private array $projectExtensionFiles, + private array $currentFileHashes, ) { } @@ -101,7 +106,7 @@ public function getUnmatchedLineIgnores(): array } /** - * @return array> + * @return CollectorData */ public function getCollectedData(): array { @@ -116,6 +121,14 @@ public function getDependencies(): array return $this->dependencies; } + /** + * @return array> + */ + public function getUsedTraitDependencies(): array + { + return $this->usedTraitDependencies; + } + /** * @return array> */ @@ -132,4 +145,12 @@ 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 d9cbee81c6..8f1ce5a91c 100644 --- a/src/Analyser/ResultCache/ResultCacheClearer.php +++ b/src/Analyser/ResultCache/ResultCacheClearer.php @@ -2,14 +2,20 @@ namespace PHPStan\Analyser\ResultCache; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use function dirname; use function is_file; use function unlink; -class ResultCacheClearer +#[AutowiredService] +final class ResultCacheClearer { - public function __construct(private string $cacheFilePath) + public function __construct( + #[AutowiredParameter(ref: '%resultCachePath%')] + private string $cacheFilePath, + ) { } diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index 416ce41108..87e6b07785 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -8,13 +8,18 @@ use PHPStan\Analyser\FileAnalyserResult; use PHPStan\Collectors\CollectedData; use PHPStan\Command\Output; +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\FileHelper; use PHPStan\File\FileWriter; +use PHPStan\Internal\ArrayHelper; use PHPStan\Internal\ComposerHelper; use PHPStan\PhpDoc\StubFilesProvider; use PHPStan\Reflection\ReflectionProvider; @@ -25,16 +30,18 @@ use function array_filter; use function array_key_exists; use function array_keys; +use function array_merge; use function array_unique; use function array_values; use function count; +use function 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 round; use function sha1_file; use function sort; use function sprintf; @@ -46,8 +53,10 @@ /** * @phpstan-import-type LinesToIgnore from FileAnalyserResult + * @phpstan-import-type CollectorData from CollectedData */ -class ResultCacheManager +#[GenerateFactory(interface: ResultCacheManagerFactory::class)] +final class ResultCacheManager { private const CACHE_VERSION = 'v12-linesToIgnore'; @@ -60,26 +69,47 @@ class ResultCacheManager /** * @param string[] $analysedPaths + * @param string[] $analysedPathsFromConfig * @param string[] $composerAutoloaderProjectPaths * @param string[] $bootstrapFiles * @param string[] $scanFiles * @param string[] $scanDirectories + * @param list> $parametersNotInvalidatingCache + * @param array $fileReplacements */ public function __construct( + 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, ) { } @@ -91,63 +121,72 @@ public function __construct( 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 (!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()) { + 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()) { + 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); } /** @@ -159,27 +198,28 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? 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']; @@ -234,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,6 +292,25 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? $cachedFileExportedNodes = $filteredExportedNodes[$analysedFile]; $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; } @@ -287,10 +350,10 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? $filesToAnalyse = array_unique($filesToAnalyse); $filesToAnalyseCount = count($filesToAnalyse); - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $elapsed = microtime(true) - $startTime; $elapsedString = $elapsed > 5 - ? sprintf(' in %f seconds', round($elapsed, 1)) + ? sprintf(' in %.1f seconds', $elapsed) : ''; $output->writeLineFormatted(sprintf( @@ -301,7 +364,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? )); } - return new ResultCache($filesToAnalyse, false, $data['lastFullAnalysisTime'], $meta, $filteredErrors, $filteredLocallyIgnoredErrors, $filteredLinesToIgnore, $filteredUnmatchedLineIgnores, $filteredCollectedData, $invertedDependenciesToReturn, $filteredExportedNodes, $data['projectExtensionFiles']); + return new ResultCache($filesToAnalyse, false, $data['lastFullAnalysisTime'], $meta, $filteredErrors, $filteredLocallyIgnoredErrors, $filteredLinesToIgnore, $filteredUnmatchedLineIgnores, $filteredCollectedData, $invertedDependenciesToReturn, $invertedUsedTraitDependenciesToReturn, $filteredExportedNodes, $data['projectExtensionFiles'], $currentFileHashes); } /** @@ -357,6 +420,9 @@ private function getMetaKeyDifferences(array $cachedMeta, array $currentMeta): a */ 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 = []; @@ -400,44 +466,54 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache $freshLocallyIgnoredErrorsByFile[$error->getFilePath()][] = $error; } - $freshCollectedDataByFile = []; - foreach ($analyserResult->getCollectedData() as $collectedData) { - $freshCollectedDataByFile[$collectedData->getFilePath()][] = $collectedData; - } + $freshCollectedDataByFile = $analyserResult->getCollectedData(); $meta = $resultCache->getMeta(); $projectConfigArray = $meta['projectConfig']; if ($projectConfigArray !== null) { $meta['projectConfig'] = Neon::encode($projectConfigArray); } - $doSave = function (array $errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, ?array $dependencies, array $exportedNodes, array $projectExtensionFiles) use ($internalErrors, $resultCache, $output, $onlyFiles, $meta): bool { + $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())); } @@ -445,9 +521,9 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache } } - $this->save($resultCache->getLastFullAnalysisTime(), $errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, $dependencies, $exportedNodes, $projectExtensionFiles, $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.'); } @@ -461,9 +537,9 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache if ($analyserResult->getDependencies() !== null) { $projectExtensionFiles = $this->getProjectExtensionFiles($projectConfigArray, $analyserResult->getDependencies()); } - $saved = $doSave($freshErrorsByFile, $freshLocallyIgnoredErrorsByFile, $analyserResult->getLinesToIgnore(), $analyserResult->getUnmatchedLineIgnores(), $freshCollectedDataByFile, $analyserResult->getDependencies(), $analyserResult->getExportedNodes(), $projectExtensionFiles); + $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.'); } } @@ -474,7 +550,8 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache $errorsByFile = $this->mergeErrors($resultCache, $freshErrorsByFile); $locallyIgnoredErrorsByFile = $this->mergeLocallyIgnoredErrors($resultCache, $freshLocallyIgnoredErrorsByFile); $collectedDataByFile = $this->mergeCollectedData($resultCache, $freshCollectedDataByFile); - $dependencies = $this->mergeDependencies($resultCache, $analyserResult->getDependencies()); + $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()); @@ -501,7 +578,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache $projectExtensionFiles[$file] = [$hash, true, $className]; } } - $saved = $doSave($errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, $dependencies, $exportedNodes, $projectExtensionFiles); + $saved = $doSave($errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, $dependencies, $usedTraitDependencies, $exportedNodes, $projectExtensionFiles); } $flatErrors = []; @@ -518,13 +595,6 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache } } - $flatCollectedData = []; - foreach ($collectedDataByFile as $fileCollectedData) { - foreach ($fileCollectedData as $collectedData) { - $flatCollectedData[] = $collectedData; - } - } - return new ResultCacheProcessResult(new AnalyserResult( $flatErrors, $analyserResult->getFilteredPhpErrors(), @@ -533,8 +603,9 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache $linesToIgnore, $unmatchedLineIgnores, $internalErrors, - $flatCollectedData, + $collectedDataByFile, $dependencies, + $usedTraitDependencies, $exportedNodes, $analyserResult->hasReachedInternalErrorsCountLimit(), $analyserResult->getPeakMemoryUsageBytes(), @@ -549,6 +620,10 @@ private function mergeErrors(ResultCache $resultCache, array $freshErrorsByFile) { $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; @@ -567,6 +642,10 @@ private function mergeLocallyIgnoredErrors(ResultCache $resultCache, array $fres { $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; @@ -578,13 +657,17 @@ private function mergeLocallyIgnoredErrors(ResultCache $resultCache, array $fres } /** - * @param array> $freshCollectedDataByFile - * @return array> + * @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; @@ -596,17 +679,18 @@ private function mergeCollectedData(ResultCache $resultCache, array $freshCollec } /** + * @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) { @@ -624,7 +708,11 @@ private function mergeDependencies(ResultCache $resultCache, ?array $freshDepend } $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; @@ -644,6 +732,10 @@ private function mergeExportedNodes(ResultCache $resultCache, array $freshExport { $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; @@ -663,6 +755,10 @@ private function mergeLinesToIgnore(ResultCache $resultCache, array $freshLinesT { $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; @@ -682,6 +778,10 @@ private function mergeUnmatchedLineIgnores(ResultCache $resultCache, array $fres { $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; @@ -698,10 +798,12 @@ private function mergeUnmatchedLineIgnores(ResultCache $resultCache, array $fres * @param array> $locallyIgnoredErrors * @param array $linesToIgnore * @param array $unmatchedLineIgnores - * @param array> $collectedData + * @param array>> $collectedData * @param array> $dependencies + * @param array> $usedTraitDependencies * @param array> $exportedNodes * @param array $projectExtensionFiles + * @param array $currentFileHashes * @param mixed[] $meta */ private function save( @@ -712,8 +814,10 @@ private function save( array $unmatchedLineIgnores, array $collectedData, array $dependencies, + array $usedTraitDependencies, array $exportedNodes, array $projectExtensionFiles, + array $currentFileHashes, array $meta, ): void { @@ -723,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]); @@ -732,6 +836,20 @@ 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 ShouldNotHappenException(); @@ -742,7 +860,7 @@ private function save( } $invertedDependencies[$file] = [ - 'fileHash' => $this->getFileHash($file), + 'fileHash' => $currentFileHashes[$file] ?? $this->getFileHash($file), 'dependentFiles' => [], ]; } @@ -754,10 +872,22 @@ private function save( ksort($collectedData); ksort($invertedDependencies); + foreach ($collectedData as & $collectedDataPerFile) { + ksort($collectedDataPerFile); + } + foreach ($invertedDependencies as $file => $fileData) { $dependentFiles = $fileData['dependentFiles']; sort($dependentFiles); $invertedDependencies[$file]['dependentFiles'] = $dependentFiles; + + $usedTraitDependentFiles = $fileData['usedTraitDependentFiles'] ?? []; + if (count($usedTraitDependentFiles) === 0) { + continue; + } + + sort($usedTraitDependentFiles); + $invertedDependencies[$file]['usedTraitDependentFiles'] = $usedTraitDependentFiles; } ksort($exportedNodes); @@ -885,18 +1015,10 @@ private function getMeta(array $allAnalysedFiles, ?array $projectConfigArray): a sort($extensions); if ($projectConfigArray !== null) { - unset($projectConfigArray['parameters']['editorUrl']); - unset($projectConfigArray['parameters']['editorUrlTitle']); - unset($projectConfigArray['parameters']['errorFormat']); - unset($projectConfigArray['parameters']['ignoreErrors']); - unset($projectConfigArray['parameters']['reportUnmatchedIgnoredErrors']); - unset($projectConfigArray['parameters']['tipsOfTheDay']); - unset($projectConfigArray['parameters']['parallel']); - unset($projectConfigArray['parameters']['internalErrorsCountLimit']); - unset($projectConfigArray['parameters']['cache']); - unset($projectConfigArray['parameters']['memoryLimitFile']); - unset($projectConfigArray['parameters']['pro']); - unset($projectConfigArray['parametersSchema']); + foreach ($this->parametersNotInvalidatingCache as $parameterPath) { + $pathAsArray = is_array($parameterPath) ? $parameterPath : explode('.', $parameterPath); + ArrayHelper::unsetKeyAtPath($projectConfigArray, $pathAsArray); + } ksort($projectConfigArray); } @@ -904,6 +1026,7 @@ private function getMeta(array $allAnalysedFiles, ?array $projectConfigArray): a return [ 'cacheVersion' => self::CACHE_VERSION, 'phpstanVersion' => ComposerHelper::getPhpStanVersion(), + 'metaExtensions' => $this->getMetaFromPhpStanExtensions(), 'phpVersion' => PHP_VERSION_ID, 'projectConfig' => $projectConfigArray, 'analysedPaths' => $this->analysedPaths, @@ -919,6 +1042,9 @@ private function getMeta(array $allAnalysedFiles, ?array $projectConfigArray): a private function getFileHash(string $path): string { + if (array_key_exists($path, $this->fileReplacements)) { + $path = $this->fileReplacements[$path]; + } if (array_key_exists($path, $this->fileHashes)) { return $this->fileHashes[$path]; } @@ -939,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) { @@ -993,7 +1131,7 @@ private function getComposerLocks(): array } /** - * @return array + * @return array> */ private function getComposerInstalled(): array { @@ -1011,6 +1149,10 @@ private function getComposerInstalled(): array } $installed = require $filePath; + if (!is_array($installed)) { + throw new ShouldNotHappenException(); + } + $rootName = $installed['root']['name']; unset($installed['root']); unset($installed['versions'][$rootName]); @@ -1036,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 269f745015..333bc6136e 100644 --- a/src/Analyser/ResultCache/ResultCacheManagerFactory.php +++ b/src/Analyser/ResultCache/ResultCacheManagerFactory.php @@ -5,6 +5,9 @@ interface ResultCacheManagerFactory { - public function create(): ResultCacheManager; + /** + * @param array $fileReplacements + */ + 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 @@ + + */ + public function getIdenticalResult(Scope $scope, Identical $expr): TypeResult + { + if ( + $expr->left instanceof Variable + && is_string($expr->left->name) + && $expr->right instanceof Variable + && is_string($expr->right->name) + && $expr->left->name === $expr->right->name + ) { + return new TypeResult(new ConstantBooleanType(true), []); + } + + $leftType = $scope->getType($expr->left); + $rightType = $scope->getType($expr->right); + + if (!$scope instanceof MutatingScope) { + return $this->initializerExprTypeResolver->resolveIdenticalType($leftType, $rightType); + } + + if ( + ( + $expr->left instanceof Node\Expr\PropertyFetch + || $expr->left instanceof Node\Expr\StaticPropertyFetch + ) + && $rightType->isNull()->yes() + && !$scope->hasPropertyNativeType($expr->left) + ) { + return new TypeResult(new BooleanType(), []); + } + + if ( + ( + $expr->right instanceof Node\Expr\PropertyFetch + || $expr->right instanceof Node\Expr\StaticPropertyFetch + ) + && $leftType->isNull()->yes() + && !$scope->hasPropertyNativeType($expr->right) + ) { + return new TypeResult(new BooleanType(), []); + } + + return $this->initializerExprTypeResolver->resolveIdenticalType($leftType, $rightType); + } + + /** + * @return TypeResult + */ + public function getNotIdenticalResult(Scope $scope, Node\Expr\BinaryOp\NotIdentical $expr): TypeResult + { + $identicalResult = $this->getIdenticalResult($scope, new Identical($expr->left, $expr->right)); + $identicalType = $identicalResult->type; + if ($identicalType instanceof ConstantBooleanType) { + return new TypeResult(new ConstantBooleanType(!$identicalType->getValue()), $identicalResult->reasons); + } + + return new TypeResult(new BooleanType(), []); + } + +} diff --git a/src/Analyser/RuleErrorTransformer.php b/src/Analyser/RuleErrorTransformer.php index 464e8cd776..ebbc7679e9 100644 --- a/src/Analyser/RuleErrorTransformer.php +++ b/src/Analyser/RuleErrorTransformer.php @@ -2,30 +2,61 @@ namespace PHPStan\Analyser; +use PhpParser\Internal\TokenStream; use PhpParser\Node; +use PhpParser\Node\Stmt; +use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor\CloningVisitor; +use PhpParser\Parser; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\File\FileReader; +use PHPStan\Fixable\PhpPrinter; +use PHPStan\Fixable\PhpPrinterIndentationDetectorVisitor; +use PHPStan\Fixable\ReplacingNodeVisitor; +use PHPStan\Fixable\UnwrapVirtualNodesVisitor; +use PHPStan\Node\VirtualNode; use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\FixableNodeRuleError; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\LineRuleError; use PHPStan\Rules\MetadataRuleError; use PHPStan\Rules\NonIgnorableRuleError; use PHPStan\Rules\RuleError; use PHPStan\Rules\TipRuleError; -use function is_string; +use PHPStan\ShouldNotHappenException; +use SebastianBergmann\Diff\Differ; +use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder; +use function get_class; +use function sha1; +use function str_contains; +use function str_repeat; -class RuleErrorTransformer +#[AutowiredService] +final class RuleErrorTransformer { + private Differ $differ; + + public function __construct( + #[AutowiredParameter(ref: '@currentPhpVersionPhpParser')] + private Parser $parser, + ) + { + $this->differ = new Differ(new UnifiedDiffOutputBuilder('', addLineNumbers: true)); + } + /** - * @param class-string $nodeType + * @param Node\Stmt[] $fileNodes */ public function transform( - string|RuleError $ruleError, + RuleError $ruleError, Scope $scope, - string $nodeType, - int $nodeLine, + array $fileNodes, + Node $node, ): Error { - $line = $nodeLine; + $line = $node->getStartLine(); $canBeIgnored = true; $fileName = $scope->getFileDescription(); $filePath = $scope->getFile(); @@ -39,53 +70,101 @@ public function transform( $traitFilePath = $traitReflection->getFileName(); } } - if (is_string($ruleError)) { - $message = $ruleError; - } else { - $message = $ruleError->getMessage(); - if ( - $ruleError instanceof LineRuleError - && $ruleError->getLine() !== -1 - ) { - $line = $ruleError->getLine(); + + if ( + $ruleError instanceof 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'); } - if ( - $ruleError instanceof FileRuleError - && $ruleError->getFile() !== '' - ) { - $fileName = $ruleError->getFileDescription(); - $filePath = $ruleError->getFile(); - $traitFilePath = null; + $fixingFile = $filePath; + if ($traitFilePath !== null) { + $fixingFile = $traitFilePath; } - if ($ruleError instanceof TipRuleError) { - $tip = $ruleError->getTip(); - } + $oldCode = FileReader::read($fixingFile); - if ($ruleError instanceof IdentifierRuleError) { - $identifier = $ruleError->getIdentifier(); - } + $this->parser->parse($oldCode); + $hash = sha1($oldCode); + $oldTokens = $this->parser->getTokens(); - if ($ruleError instanceof MetadataRuleError) { - $metadata = $ruleError->getMetadata(); - } + $indentTraverser = new NodeTraverser(); + $indentDetector = new PhpPrinterIndentationDetectorVisitor(new TokenStream($oldTokens, PhpPrinter::TAB_WIDTH)); + $indentTraverser->addVisitor($indentDetector); + $indentTraverser->traverse($fileNodes); - if ($ruleError instanceof NonIgnorableRuleError) { - $canBeIgnored = false; + $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( - $message, + $ruleError->getMessage(), $fileName, $line, $canBeIgnored, $filePath, $traitFilePath, $tip, - $nodeLine, - $nodeType, + $node->getStartLine(), + get_class($node), $identifier, $metadata, + $fixedErrorDiff, ); } diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index e26aa8853a..1134614b2f 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -6,16 +6,18 @@ 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\ConstantReflection; 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; @@ -49,10 +51,7 @@ public function isInTrait(): bool; public function getTraitReflection(): ?ClassReflection; - /** - * @return FunctionReflection|ExtendedMethodReflection|null - */ - public function getFunction(); + public function getFunction(): ?PhpFunctionFromParserNodeReflection; public function getFunctionName(): ?string; @@ -69,18 +68,27 @@ public function canAnyVariableExist(): bool; */ public function getDefinedVariables(): array; + /** + * @return array + */ + public function getMaybeDefinedVariables(): array; + public function hasConstant(Name $name): bool; - public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?PropertyReflection; + public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection; public function getMethodReflection(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection; - public function getConstantReflection(Type $typeWithConstant, string $constantName): ?ConstantReflection; + public function getConstantReflection(Type $typeWithConstant, string $constantName): ?ClassConstantReflection; public function getIterableKeyType(Type $iteratee): Type; public function getIterableValueType(Type $iteratee): Type; + /** + * @phpstan-assert-if-true !null $this->getAnonymousFunctionReflection() + * @phpstan-assert-if-true !null $this->getAnonymousFunctionReturnType() + */ public function isInAnonymousFunction(): bool; public function getAnonymousFunctionReflection(): ?ParametersAcceptor; @@ -93,11 +101,6 @@ public function getNativeType(Expr $expr): Type; public function getKeepVoidType(Expr $node): Type; - /** - * @deprecated Use getNativeType() - */ - public function doNotTreatPhpDocTypesAsCertain(): self; - public function resolveName(Name $name): string; public function resolveTypeByName(Name $name): TypeWithClassName; @@ -107,9 +110,6 @@ public function resolveTypeByName(Name $name): TypeWithClassName; */ public function getTypeFromValue($value): Type; - /** @deprecated use hasExpressionType instead */ - public function isSpecified(Expr $node): bool; - public function hasExpressionType(Expr $node): TrinaryLogic; public function isInClassExists(string $className): bool; @@ -141,4 +141,6 @@ public function filterByFalseyValue(Expr $expr): self; public function isInFirstLevelStatement(): bool; + public function getPhpVersion(): PhpVersions; + } diff --git a/src/Analyser/ScopeContext.php b/src/Analyser/ScopeContext.php index 4118909860..dfa0c1f17b 100644 --- a/src/Analyser/ScopeContext.php +++ b/src/Analyser/ScopeContext.php @@ -5,7 +5,7 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\ShouldNotHappenException; -class ScopeContext +final class ScopeContext { private function __construct( diff --git a/src/Analyser/ScopeFactory.php b/src/Analyser/ScopeFactory.php index c2401d375c..be9ca1982d 100644 --- a/src/Analyser/ScopeFactory.php +++ b/src/Analyser/ScopeFactory.php @@ -2,8 +2,13 @@ namespace PHPStan\Analyser; -/** @api */ -class ScopeFactory +use PHPStan\DependencyInjection\AutowiredService; + +/** + * @api + */ +#[AutowiredService] +final class ScopeFactory { public function __construct(private InternalScopeFactory $internalScopeFactory) diff --git a/src/Analyser/SpecifiedTypes.php b/src/Analyser/SpecifiedTypes.php index 578a34cdc2..fd9ddda81d 100644 --- a/src/Analyser/SpecifiedTypes.php +++ b/src/Analyser/SpecifiedTypes.php @@ -6,25 +6,81 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class SpecifiedTypes +final class SpecifiedTypes { + private bool $overwrite = false; + + /** @var array */ + private array $newConditionalExpressionHolders = []; + + private ?Expr $rootExpr = null; + /** * @api * @param array $sureTypes * @param array $sureNotTypes - * @param array $newConditionalExpressionHolders */ public function __construct( private array $sureTypes = [], private array $sureNotTypes = [], - private bool $overwrite = false, - private array $newConditionalExpressionHolders = [], - private ?Expr $rootExpr = null, ) { } + /** + * Normally, $sureTypes in truthy context are used to intersect with the pre-existing type. + * And $sureNotTypes are used to remove type from the pre-existing type. + * + * Example: By default, non-empty-string intersected with '' (ConstantStringType) will lead to NeverType. + * Because it's not possible to narrow non-empty-string to an empty string. + * + * In rare cases, a type-specifying extension might want to overwrite the pre-existing types + * without taking the pre-existing types into consideration. + * + * In that case it should also call setAlwaysOverwriteTypes() on + * the returned object. + * + * ! Only do this if you're certain. Otherwise, this is a source of common bugs. ! + * + * @api + */ + public function setAlwaysOverwriteTypes(): self + { + $self = new self($this->sureTypes, $this->sureNotTypes); + $self->overwrite = true; + $self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders; + $self->rootExpr = $this->rootExpr; + + return $self; + } + + /** + * @api + */ + public function setRootExpr(?Expr $rootExpr): self + { + $self = new self($this->sureTypes, $this->sureNotTypes); + $self->overwrite = $this->overwrite; + $self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders; + $self->rootExpr = $rootExpr; + + return $self; + } + + /** + * @param array $newConditionalExpressionHolders + */ + public function setNewConditionalExpressionHolders(array $newConditionalExpressionHolders): self + { + $self = new self($this->sureTypes, $this->sureNotTypes); + $self->overwrite = $this->overwrite; + $self->newConditionalExpressionHolders = $newConditionalExpressionHolders; + $self->rootExpr = $this->rootExpr; + + return $self; + } + /** * @api * @return array @@ -90,7 +146,12 @@ public function intersectWith(SpecifiedTypes $other): self ]; } - return new self($sureTypeUnion, $sureNotTypeUnion, $this->overwrite && $other->overwrite, [], $rootExpr); + $result = new self($sureTypeUnion, $sureNotTypeUnion); + if ($this->overwrite && $other->overwrite) { + $result = $result->setAlwaysOverwriteTypes(); + } + + return $result->setRootExpr($rootExpr); } /** @api */ @@ -122,7 +183,12 @@ public function unionWith(SpecifiedTypes $other): self ]; } - return new self($sureTypeUnion, $sureNotTypeUnion, $this->overwrite || $other->overwrite, [], $rootExpr); + $result = new self($sureTypeUnion, $sureNotTypeUnion); + if ($this->overwrite || $other->overwrite) { + $result = $result->setAlwaysOverwriteTypes(); + } + + return $result->setRootExpr($rootExpr); } public function normalize(Scope $scope): self @@ -138,7 +204,12 @@ public function normalize(Scope $scope): self $sureTypes[$exprString][1] = TypeCombinator::remove($sureTypes[$exprString][1], $sureNotType); } - return new self($sureTypes, [], $this->overwrite, $this->newConditionalExpressionHolders, $this->rootExpr); + $result = new self($sureTypes, []); + if ($this->overwrite) { + $result = $result->setAlwaysOverwriteTypes(); + } + + return $result->setRootExpr($this->rootExpr); } private function mergeRootExpr(?Expr $rootExprA, ?Expr $rootExprB): ?Expr diff --git a/src/Analyser/StatementContext.php b/src/Analyser/StatementContext.php index 222d768b52..5fd5381601 100644 --- a/src/Analyser/StatementContext.php +++ b/src/Analyser/StatementContext.php @@ -2,7 +2,15 @@ namespace PHPStan\Analyser; -class StatementContext +/** + * Object of this class is one of the parameters of `NodeScopeResolver::processStmtNodes()`. + * + * It determines whether loops will be analysed once or multiple times + * until the types "stabilize". + * + * When in doubt, use `StatementContext::createTopLevel()`. + */ +final class StatementContext { private function __construct( @@ -11,11 +19,17 @@ private function __construct( { } + /** + * @api + */ public static function createTopLevel(): self { return new self(true); } + /** + * @api + */ public static function createDeep(): self { return new self(false); diff --git a/src/Analyser/StatementExitPoint.php b/src/Analyser/StatementExitPoint.php index f7f46ca091..5c4916373e 100644 --- a/src/Analyser/StatementExitPoint.php +++ b/src/Analyser/StatementExitPoint.php @@ -4,8 +4,10 @@ use PhpParser\Node\Stmt; -/** @api */ -class StatementExitPoint +/** + * @api + */ +final class StatementExitPoint { public function __construct(private Stmt $statement, private MutatingScope $scope) diff --git a/src/Analyser/StatementResult.php b/src/Analyser/StatementResult.php index 20365299c0..dad528dc18 100644 --- a/src/Analyser/StatementResult.php +++ b/src/Analyser/StatementResult.php @@ -2,11 +2,13 @@ namespace PHPStan\Analyser; -use PhpParser\Node\Scalar\LNumber; +use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt; -/** @api */ -class StatementResult +/** + * @api + */ +final class StatementResult { /** @@ -55,7 +57,7 @@ public function filterOutLoopExitPoints(): self } $num = $statement->num; - if (!$num instanceof LNumber) { + if (!$num instanceof Int_) { return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints); } @@ -79,7 +81,7 @@ public function getExitPoints(): array /** * @param class-string|class-string $stmtClass - * @return StatementExitPoint[] + * @return list */ public function getExitPointsByType(string $stmtClass): array { @@ -96,7 +98,7 @@ public function getExitPointsByType(string $stmtClass): array continue; } - if (!$value instanceof LNumber) { + if (!$value instanceof Int_) { $exitPoints[] = $exitPoint; continue; } @@ -113,7 +115,7 @@ public function getExitPointsByType(string $stmtClass): array } /** - * @return StatementExitPoint[] + * @return list */ public function getExitPointsForOuterLoop(): array { @@ -127,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; @@ -137,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); diff --git a/src/Analyser/ThrowPoint.php b/src/Analyser/ThrowPoint.php index 26ceec4260..873c11e425 100644 --- a/src/Analyser/ThrowPoint.php +++ b/src/Analyser/ThrowPoint.php @@ -8,8 +8,10 @@ use PHPStan\Type\TypeCombinator; use Throwable; -/** @api */ -class ThrowPoint +/** + * @api + */ +final class ThrowPoint { /** diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index f35b81a939..e946229f7d 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -19,15 +19,18 @@ 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\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\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ResolvedFunctionVariant; +use PHPStan\Rules\Arrays\AllowedArrayKeysTypes; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; @@ -39,8 +42,10 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\ConditionalTypeForParameter; +use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantScalarType; @@ -82,7 +87,8 @@ use function substr; use const COUNT_NORMAL; -class TypeSpecifier +#[AutowiredService(name: 'typeSpecifier', factory: '@typeSpecifierFactory::create')] +final class TypeSpecifier { /** @var MethodTypeSpecifyingExtension[][]|null */ @@ -99,6 +105,7 @@ class TypeSpecifier public function __construct( private ExprPrinter $exprPrinter, private ReflectionProvider $reflectionProvider, + private PhpVersion $phpVersion, private array $functionTypeSpecifyingExtensions, private array $methodTypeSpecifyingExtensions, private array $staticMethodTypeSpecifyingExtensions, @@ -119,13 +126,10 @@ public function specifyTypesInCondition( Scope $scope, Expr $expr, TypeSpecifierContext $context, - ?Expr $rootExpr = null, ): SpecifiedTypes { - $rootExpr ??= $expr; - if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { - return new SpecifiedTypes([], [], false, [], $rootExpr); + return (new SpecifiedTypes([], []))->setRootExpr($expr); } if ($expr instanceof Instanceof_) { @@ -149,18 +153,21 @@ public function specifyTypesInCondition( } else { $type = new ObjectType($className); } - return $this->create($exprNode, $type, $context, false, $scope, $rootExpr); + 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->getObjectClassNames() !== []) { + $uncertainty = true; return $type; } if ($type instanceof GenericClassStringType) { + $uncertainty = true; return $type->getGenericType(); } if ($type instanceof ConstantStringType) { @@ -175,36 +182,58 @@ public function specifyTypesInCondition( $type, new ObjectWithoutClassType(), ); - return $this->create($exprNode, $type, $context, false, $scope, $rootExpr); - } 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, $rootExpr); + return $this->create($exprNode, $type, $context, $scope)->setRootExpr($expr); } } } if ($context->true()) { - return $this->create($exprNode, new ObjectWithoutClassType(), $context, false, $scope, $rootExpr); + return $this->create($exprNode, new ObjectWithoutClassType(), $context, $scope)->setRootExpr($exprNode); } } elseif ($expr instanceof Node\Expr\BinaryOp\Identical) { - return $this->resolveIdentical($expr, $scope, $context, $rootExpr); + 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, - $rootExpr, - ); + )->setRootExpr($expr); + } elseif ($expr instanceof Expr\Cast\Bool_) { + return $this->specifyTypesInCondition( + $scope, + new Node\Expr\BinaryOp\Equal($expr->expr, new ConstFetch(new Name\FullyQualified('true'))), + $context, + )->setRootExpr($expr); + } elseif ($expr instanceof Expr\Cast\String_) { + return $this->specifyTypesInCondition( + $scope, + new Node\Expr\BinaryOp\NotEqual($expr->expr, new Node\Scalar\String_('')), + $context, + )->setRootExpr($expr); + } elseif ($expr instanceof Expr\Cast\Int_) { + return $this->specifyTypesInCondition( + $scope, + new Node\Expr\BinaryOp\NotEqual($expr->expr, new Node\Scalar\LNumber(0)), + $context, + )->setRootExpr($expr); + } elseif ($expr instanceof Expr\Cast\Double) { + return $this->specifyTypesInCondition( + $scope, + new Node\Expr\BinaryOp\NotEqual($expr->expr, new Node\Scalar\DNumber(0.0)), + $context, + )->setRootExpr($expr); } elseif ($expr instanceof Node\Expr\BinaryOp\Equal) { - return $this->resolveEqual($expr, $scope, $context, $rootExpr); + 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, - $rootExpr, - ); + )->setRootExpr($expr); } elseif ($expr instanceof Node\Expr\BinaryOp\Smaller || $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual) { @@ -212,11 +241,11 @@ public function specifyTypesInCondition( $expr->left instanceof FuncCall && count($expr->left->getArgs()) >= 1 && $expr->left->name instanceof Name - && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen'], 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', 'mb_strlen'], true) + || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) ) ) { $inverseOperator = $expr instanceof Node\Expr\BinaryOp\Smaller @@ -227,14 +256,13 @@ public function specifyTypesInCondition( $scope, new Node\Expr\BooleanNot($inverseOperator), $context, - $rootExpr, - ); + )->setRootExpr($expr); } $orEqual = $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual; $offset = $orEqual ? 0 : 1; $leftType = $scope->getType($expr->left); - $result = new SpecifiedTypes([], [], false, [], $rootExpr); + $result = (new SpecifiedTypes([], []))->setRootExpr($expr); if ( !$context->null() @@ -246,22 +274,21 @@ public function specifyTypesInCondition( ) { $argType = $scope->getType($expr->right->getArgs()[0]->value); - if ($argType instanceof UnionType) { - $sizeType = null; - if ($leftType instanceof ConstantIntegerType) { - if ($orEqual) { - $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); - } else { - $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); - } - } elseif ($leftType instanceof IntegerRangeType) { - $sizeType = $leftType; + 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; + } - $narrowed = $this->narrowUnionByArraySize($expr->right, $argType, $sizeType, $context, $scope, $rootExpr); - if ($narrowed !== null) { - return $narrowed; - } + $specifiedTypes = $this->specifyTypesForCountFuncCall($expr->right, $argType, $sizeType, $context, $scope, $expr); + if ($specifiedTypes !== null) { + $result = $result->unionWith($specifiedTypes); } if ( @@ -289,23 +316,40 @@ public function specifyTypesInCondition( if (count($countables) > 0) { $countableType = TypeCombinator::union(...$countables); - return $this->create($expr->right->getArgs()[0]->value, $countableType, $context, false, $scope, $rootExpr); + return $this->create($expr->right->getArgs()[0]->value, $countableType, $context, $scope)->setRootExpr($expr); } } if ($argType->isArray()->yes()) { $newType = new NonEmptyArrayType(); if ($context->true() && $argType->isList()->yes()) { - $newType = AccessoryArrayListType::intersectWith($newType); + $newType = TypeCombinator::intersect($newType, new AccessoryArrayListType()); } $result = $result->unionWith( - $this->create($expr->right->getArgs()[0]->value, $newType, $context, false, $scope, $rootExpr), + $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 @@ -326,7 +370,7 @@ public function specifyTypesInCondition( $accessory = new AccessoryNonFalsyStringType(); } - $result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, $accessory, $context, false, $scope, $rootExpr)); + $result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, $accessory, $context, $scope)->setRootExpr($expr)); } } } @@ -334,21 +378,21 @@ public function specifyTypesInCondition( if ($leftType instanceof ConstantIntegerType) { if ($expr->right instanceof Expr\PostInc) { $result = $result->unionWith($this->createRangeTypes( - $rootExpr, + $expr, $expr->right->var, IntegerRangeType::fromInterval($leftType->getValue(), null, $offset + 1), $context, )); } elseif ($expr->right instanceof Expr\PostDec) { $result = $result->unionWith($this->createRangeTypes( - $rootExpr, + $expr, $expr->right->var, IntegerRangeType::fromInterval($leftType->getValue(), null, $offset - 1), $context, )); } elseif ($expr->right instanceof Expr\PreInc || $expr->right instanceof Expr\PreDec) { $result = $result->unionWith($this->createRangeTypes( - $rootExpr, + $expr, $expr->right->var, IntegerRangeType::fromInterval($leftType->getValue(), null, $offset), $context, @@ -360,21 +404,21 @@ public function specifyTypesInCondition( if ($rightType instanceof ConstantIntegerType) { if ($expr->left instanceof Expr\PostInc) { $result = $result->unionWith($this->createRangeTypes( - $rootExpr, + $expr, $expr->left->var, IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset + 1), $context, )); } elseif ($expr->left instanceof Expr\PostDec) { $result = $result->unionWith($this->createRangeTypes( - $rootExpr, + $expr, $expr->left->var, IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset - 1), $context, )); } elseif ($expr->left instanceof Expr\PreInc || $expr->left instanceof Expr\PreDec) { $result = $result->unionWith($this->createRangeTypes( - $rootExpr, + $expr, $expr->left->var, IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset), $context, @@ -387,24 +431,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, - $rootExpr, - ), + )->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, - $rootExpr, - ), + )->setRootExpr($expr), ); } } elseif ($context->false()) { @@ -412,24 +452,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, - $rootExpr, - ), + )->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, - $rootExpr, - ), + )->setRootExpr($expr), ); } } @@ -437,27 +473,32 @@ 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, $rootExpr); + 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, $rootExpr); + 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); } - // lazy create parametersAcceptor, as creation can be expensive - $parametersAcceptor = null; if (count($expr->getArgs()) > 0) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants()); - $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; @@ -471,7 +512,7 @@ public function specifyTypesInCondition( $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( $type, $parametersAcceptor->getResolvedTemplateTypeMap(), - $parametersAcceptor instanceof ParametersAcceptorWithPhpDocs ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createInvariant(), )); $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); @@ -481,11 +522,20 @@ public function specifyTypesInCondition( } } - return $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope); + return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); } elseif ($expr instanceof MethodCall && $expr->name instanceof Node\Identifier) { $methodCalledOnType = $scope->getType($expr->var); $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 @@ -493,19 +543,15 @@ public function specifyTypesInCondition( ) { $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); } } - // lazy create parametersAcceptor, as creation can be expensive - $parametersAcceptor = null; if (count($expr->getArgs()) > 0) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); - $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; @@ -519,7 +565,7 @@ public function specifyTypesInCondition( $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( $type, $parametersAcceptor->getResolvedTemplateTypeMap(), - $parametersAcceptor instanceof ParametersAcceptorWithPhpDocs ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createInvariant(), )); $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); @@ -529,7 +575,7 @@ public function specifyTypesInCondition( } } - return $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope); + return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); } elseif ($expr instanceof StaticCall && $expr->name instanceof Node\Identifier) { if ($expr->class instanceof Name) { $calleeType = $scope->resolveTypeByName($expr->class); @@ -539,6 +585,15 @@ public function specifyTypesInCondition( $staticMethodReflection = $scope->getMethodReflection($calleeType, $expr->name->name); if ($staticMethodReflection !== null) { + // 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 @@ -546,19 +601,15 @@ public function specifyTypesInCondition( ) { $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); } } - // lazy create parametersAcceptor, as creation can be expensive - $parametersAcceptor = null; if (count($expr->getArgs()) > 0) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants()); - $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; @@ -572,7 +623,7 @@ public function specifyTypesInCondition( $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( $type, $parametersAcceptor->getResolvedTemplateTypeMap(), - $parametersAcceptor instanceof ParametersAcceptorWithPhpDocs ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createInvariant(), )); $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); @@ -582,28 +633,25 @@ public function specifyTypesInCondition( } } - return $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope); + return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); } elseif ($expr instanceof BooleanAnd || $expr instanceof LogicalAnd) { if (!$scope instanceof MutatingScope) { throw new ShouldNotHappenException(); } - $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context, $rootExpr); + $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr); $rightScope = $scope->filterByTruthyValue($expr->left); - $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context, $rootExpr); + $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->processBooleanNotSureConditionalTypes($scope, $leftTypes, $rightTypes), - $this->processBooleanNotSureConditionalTypes($scope, $rightTypes, $leftTypes), - $this->processBooleanSureConditionalTypes($scope, $leftTypes, $rightTypes), - $this->processBooleanSureConditionalTypes($scope, $rightTypes, $leftTypes), - ), - $rootExpr, - ); + ))->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; @@ -611,37 +659,108 @@ public function specifyTypesInCondition( if (!$scope instanceof MutatingScope) { throw new ShouldNotHappenException(); } - $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context, $rootExpr); + $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr); $rightScope = $scope->filterByFalseyValue($expr->left); - $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context, $rootExpr); + $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->processBooleanNotSureConditionalTypes($scope, $leftTypes, $rightTypes), - $this->processBooleanNotSureConditionalTypes($scope, $rightTypes, $leftTypes), - $this->processBooleanSureConditionalTypes($scope, $leftTypes, $rightTypes), - $this->processBooleanSureConditionalTypes($scope, $rightTypes, $leftTypes), - ), - $rootExpr, - ); + ))->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(), $rootExpr); + return $this->specifyTypesInCondition($scope, $expr->expr, $context->negate())->setRootExpr($expr); } elseif ($expr instanceof Node\Expr\Assign) { if (!$scope instanceof MutatingScope) { throw new ShouldNotHappenException(); } + if ($context->null()) { - return $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context, $rootExpr); + $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, $rootExpr); + $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 @@ -669,7 +788,7 @@ public function specifyTypesInCondition( throw new ShouldNotHappenException(); } - return $this->specifyTypesInCondition($scope, $andChain, $context, $rootExpr); + return $this->specifyTypesInCondition($scope, $andChain, $context)->setRootExpr($expr); } $issetExpr = $expr->vars[0]; @@ -691,10 +810,8 @@ public function specifyTypesInCondition( $issetExpr, new NullType(), $context->negate(), - false, $scope, - $rootExpr, - ); + )->setRootExpr($expr); if ($issetExpr instanceof Expr\Variable && is_string($issetExpr->name)) { if ($isset === true) { @@ -707,10 +824,8 @@ public function specifyTypesInCondition( new IssetExpr($issetExpr), new NullType(), $context, - false, $scope, - $rootExpr, - )); + ))->setRootExpr($expr); } if ($isNullable) { @@ -719,10 +834,8 @@ public function specifyTypesInCondition( new IssetExpr($issetExpr), new NullType(), $context->negate(), - false, $scope, - $rootExpr, - )); + ))->setRootExpr($expr); } // variable cannot exist in !isset() @@ -730,10 +843,8 @@ public function specifyTypesInCondition( new IssetExpr($issetExpr), new NullType(), $context, - false, $scope, - $rootExpr, - ); + )->setRootExpr($expr); } if ($isNullable && $isset === true) { @@ -767,7 +878,7 @@ public function specifyTypesInCondition( if ($var instanceof Expr\Variable && is_string($var->name)) { if ($scope->hasVariableType($var->name)->no()) { - return new SpecifiedTypes([], [], false, [], $rootExpr); + return (new SpecifiedTypes([], []))->setRootExpr($expr); } } @@ -784,11 +895,22 @@ public function specifyTypesInCondition( $var->var, new HasOffsetType($dimType), $context, - false, $scope, - $rootExpr, - ), + )->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), + ); + } } } @@ -800,7 +922,7 @@ public function specifyTypesInCondition( $this->create($var->var, new IntersectionType([ new ObjectWithoutClassType(), new HasPropertyType($var->name->toString()), - ]), TypeSpecifierContext::createTruthy(), false, $scope, $rootExpr), + ]), TypeSpecifierContext::createTruthy(), $scope)->setRootExpr($expr), ); } elseif ( $var instanceof StaticPropertyFetch @@ -811,12 +933,12 @@ public function specifyTypesInCondition( $this->create($var->class, new IntersectionType([ new ObjectWithoutClassType(), new HasPropertyType($var->name->toString()), - ]), TypeSpecifierContext::createTruthy(), false, $scope, $rootExpr), + ]), TypeSpecifierContext::createTruthy(), $scope)->setRootExpr($expr), ); } $types = $types->unionWith( - $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope, $rootExpr), + $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr), ); } @@ -840,10 +962,8 @@ public function specifyTypesInCondition( $expr->left, new NullType(), $context->negate(), - false, $scope, - $rootExpr, - ); + )->setRootExpr($expr); } if ((new ConstantBooleanType(false))->isSuperTypeOf($scope->getType($expr->right)->toBoolean())->yes()) { @@ -851,10 +971,8 @@ public function specifyTypesInCondition( $expr->left, new NullType(), TypeSpecifierContext::createFalse(), - false, $scope, - $rootExpr, - ); + )->setRootExpr($expr); } } elseif ( @@ -872,9 +990,9 @@ public function specifyTypesInCondition( return $this->specifyTypesInCondition($scope, new BooleanOr( new Expr\BooleanNot(new Expr\Isset_([$expr->expr])), new Expr\BooleanNot($expr->expr), - ), $context, $rootExpr); + ), $context)->setRootExpr($expr); } elseif ($expr instanceof Expr\ErrorSuppress) { - return $this->specifyTypesInCondition($scope, $expr->expr, $context, $rootExpr); + return $this->specifyTypesInCondition($scope, $expr->expr, $context)->setRootExpr($expr); } elseif ( $expr instanceof Expr\Ternary && !$context->null() @@ -885,7 +1003,7 @@ public function specifyTypesInCondition( $conditionExpr = new BooleanAnd($conditionExpr, $expr->if); } - return $this->specifyTypesInCondition($scope, $conditionExpr, $context, $rootExpr); + return $this->specifyTypesInCondition($scope, $conditionExpr, $context)->setRootExpr($expr); } elseif ($expr instanceof Expr\NullsafePropertyFetch && !$context->null()) { $types = $this->specifyTypesInCondition( @@ -895,10 +1013,9 @@ public function specifyTypesInCondition( new PropertyFetch($expr->var, $expr->name), ), $context, - $rootExpr, - ); + )->setRootExpr($expr); - $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope); + $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope)); } elseif ($expr instanceof Expr\NullsafeMethodCall && !$context->null()) { $types = $this->specifyTypesInCondition( @@ -908,10 +1025,9 @@ public function specifyTypesInCondition( new MethodCall($expr->var, $expr->name, $expr->args), ), $context, - $rootExpr, - ); + )->setRootExpr($expr); - $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope); + $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope)); } elseif ( $expr instanceof Expr\New_ @@ -930,7 +1046,7 @@ public function specifyTypesInCondition( $asserts = $asserts->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( $type, $parametersAcceptor->getResolvedTemplateTypeMap(), - $parametersAcceptor instanceof ParametersAcceptorWithPhpDocs ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createInvariant(), )); @@ -942,129 +1058,132 @@ public function specifyTypesInCondition( } } } elseif (!$context->null()) { - return $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope); + return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); } - return new SpecifiedTypes([], [], false, [], $rootExpr); + return (new SpecifiedTypes([], []))->setRootExpr($expr); } - private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argType, ?Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes + private function specifyTypesForCountFuncCall( + FuncCall $countFuncCall, + Type $type, + Type $sizeType, + TypeSpecifierContext $context, + Scope $scope, + Expr $rootExpr, + ): ?SpecifiedTypes { - if ($sizeType === null) { - return null; - } - if (count($countFuncCall->getArgs()) === 1) { $isNormalCount = TrinaryLogic::createYes(); } else { $mode = $scope->getType($countFuncCall->getArgs()[1]->value); - $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate()); + $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() - && $argType->isConstantArray()->yes() + !$isNormalCount->yes() + || (!$isConstantArray->yes() && !$isList->yes()) + || !$oneOrMore->isSuperTypeOf($sizeType)->yes() + || $sizeType->isSuperTypeOf($type->getArraySize())->yes() ) { - $result = []; - foreach ($argType->getTypes() as $innerType) { - $arraySize = $innerType->getArraySize(); - $isSize = $sizeType->isSuperTypeOf($arraySize); - if ($context->truthy()) { - if ($isSize->no()) { - continue; - } - - $constArray = $this->turnListIntoConstantArray($countFuncCall, $innerType, $sizeType, $scope); - if ($constArray !== null) { - $innerType = $constArray; - } - } - if ($context->falsey()) { - if (!$isSize->yes()) { - continue; - } - } - - $result[] = $innerType; - } - - return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$result), $context, false, $scope, $rootExpr); + return null; } - return null; - } - - private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, Type $sizeType, Scope $scope): ?Type - { - $argType = $scope->getType($countFuncCall->getArgs()[0]->value); - - if (count($countFuncCall->getArgs()) === 1) { - $isNormalCount = TrinaryLogic::createYes(); - } else { - $mode = $scope->getType($countFuncCall->getArgs()[1]->value); - $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate()); - } + $resultTypes = []; + foreach ($type->getArrays() as $arrayType) { + $isSizeSuperTypeOfArraySize = $sizeType->isSuperTypeOf($arrayType->getArraySize()); + if ($isSizeSuperTypeOfArraySize->no()) { + continue; + } - if ( - $isNormalCount->yes() - && $type->isList()->yes() - && $sizeType instanceof ConstantIntegerType - && $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT - ) { - // turn optional offsets non-optional - $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); - for ($i = 0; $i < $sizeType->getValue(); $i++) { - $offsetType = new ConstantIntegerType($i); - $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType)); + if ($context->falsey() && $isSizeSuperTypeOfArraySize->maybe()) { + continue; } - return $valueTypesBuilder->getArray(); - } - if ( - $isNormalCount->yes() - && $type->isList()->yes() - && $sizeType instanceof IntegerRangeType - && $sizeType->getMin() !== null - ) { - // turn optional offsets non-optional - $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); - for ($i = 0; $i < $sizeType->getMin(); $i++) { - $offsetType = new ConstantIntegerType($i); - $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType)); - } - if ($sizeType->getMax() !== null) { - for ($i = $sizeType->getMin(); $i < $sizeType->getMax(); $i++) { + if ( + $sizeType instanceof ConstantIntegerType + && $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT + && $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, $sizeType->getValue() - 1))->yes() + ) { + // turn optional offsets non-optional + $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); + for ($i = 0; $i < $sizeType->getValue(); $i++) { $offsetType = new ConstantIntegerType($i); - $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), true); + $valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType)); } - } else { - for ($i = $sizeType->getMin();; $i++) { + $resultTypes[] = $valueTypesBuilder->getArray(); + continue; + } + + if ( + $sizeType instanceof IntegerRangeType + && $sizeType->getMin() !== null + && $sizeType->getMin() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT + && $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, ($sizeType->getMax() ?? $sizeType->getMin()) - 1))->yes() + ) { + $builderData = []; + // turn optional offsets non-optional + for ($i = 0; $i < $sizeType->getMin(); $i++) { $offsetType = new ConstantIntegerType($i); - $hasOffset = $type->hasOffsetValueType($offsetType); - if ($hasOffset->no()) { - break; + $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]; } - $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), !$hasOffset->yes()); + } 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; } - return $valueTypesBuilder->getArray(); + + $resultTypes[] = $arrayType; } - return null; + return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$resultTypes), $context, $scope)->setRootExpr($rootExpr); } private function specifyTypesForConstantBinaryExpression( Expr $exprNode, - ConstantScalarType $constantType, + Type $constantType, TypeSpecifierContext $context, Scope $scope, - ?Expr $rootExpr, + Expr $rootExpr, ): ?SpecifiedTypes { - if (!$context->null() && $constantType->getValue() === false) { - $types = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - if ($exprNode instanceof Expr\NullsafeMethodCall || $exprNode instanceof Expr\NullsafePropertyFetch) { + 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; } @@ -1072,13 +1191,12 @@ private function specifyTypesForConstantBinaryExpression( $scope, $exprNode, $context->true() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createFalse()->negate(), - $rootExpr, - )); + )->setRootExpr($rootExpr)); } - if (!$context->null() && $constantType->getValue() === true) { - $types = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - if ($exprNode instanceof Expr\NullsafeMethodCall || $exprNode instanceof Expr\NullsafePropertyFetch) { + 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; } @@ -1086,85 +1204,7 @@ private function specifyTypesForConstantBinaryExpression( $scope, $exprNode, $context->true() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createTrue()->negate(), - $rootExpr, - )); - } - - if ($constantType->getValue() === null) { - return $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - } - - 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 - ) { - $argType = $scope->getType($exprNode->getArgs()[0]->value); - - if ($argType instanceof UnionType) { - $narrowed = $this->narrowUnionByArraySize($exprNode, $argType, $constantType, $context, $scope, $rootExpr); - if ($narrowed !== null) { - return $narrowed; - } - } - - if ($context->truthy() || $constantType->getValue() === 0) { - $newContext = $context; - if ($constantType->getValue() === 0) { - $newContext = $newContext->negate(); - } - - if ($argType->isArray()->yes()) { - if ( - $context->truthy() - && $argType->isConstantArray()->yes() - && $constantType->isSuperTypeOf($argType->getArraySize())->no() - ) { - return $this->create($exprNode->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr); - } - - $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - $constArray = $this->turnListIntoConstantArray($exprNode, $argType, $constantType, $scope); - if ($context->truthy() && $constArray !== null) { - $valueTypes = $this->create($exprNode->getArgs()[0]->value, $constArray, $context, false, $scope, $rootExpr); - } else { - $valueTypes = $this->create($exprNode->getArgs()[0]->value, new NonEmptyArrayType(), $newContext, false, $scope, $rootExpr); - } - return $funcTypes->unionWith($valueTypes); - } - } - } - - if ( - !$context->null() - && $exprNode instanceof FuncCall - && count($exprNode->getArgs()) === 1 - && $exprNode->name instanceof Name - && in_array(strtolower((string) $exprNode->name), ['strlen', 'mb_strlen'], 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->isString()->yes()) { - $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - - $accessory = new AccessoryNonEmptyStringType(); - if ($constantType->getValue() >= 2) { - $accessory = new AccessoryNonFalsyStringType(); - } - $valueTypes = $this->create($exprNode->getArgs()[0]->value, $accessory, $newContext, false, $scope, $rootExpr); - - return $funcTypes->unionWith($valueTypes); - } - } - + )->setRootExpr($rootExpr)); } return null; @@ -1172,46 +1212,17 @@ private function specifyTypesForConstantBinaryExpression( private function specifyTypesForConstantStringBinaryExpression( Expr $exprNode, - ConstantStringType $constantType, + Type $constantType, TypeSpecifierContext $context, Scope $scope, - ?Expr $rootExpr, + Expr $rootExpr, ): ?SpecifiedTypes { - if ( - $context->truthy() - && $exprNode instanceof FuncCall - && $exprNode->name instanceof Name - && in_array(strtolower($exprNode->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($exprNode->getArgs()[0]) - && $constantType->getValue() !== '' - ) { - $argType = $scope->getType($exprNode->getArgs()[0]->value); - - if ($argType->isString()->yes()) { - if ($constantType->getValue() !== '0') { - return $this->create( - $exprNode->getArgs()[0]->value, - TypeCombinator::intersect($argType, new AccessoryNonFalsyStringType()), - $context, - false, - $scope, - ); - } - - return $this->create( - $exprNode->getArgs()[0]->value, - TypeCombinator::intersect($argType, new AccessoryNonEmptyStringType()), - $context, - false, - $scope, - ); - } + $scalarValues = $constantType->getConstantScalarValues(); + if (count($scalarValues) !== 1 || !is_string($scalarValues[0])) { + return null; } + $constantStringValue = $scalarValues[0]; if ( $exprNode instanceof FuncCall @@ -1220,34 +1231,34 @@ private function specifyTypesForConstantStringBinaryExpression( && isset($exprNode->getArgs()[0]) ) { $type = null; - if ($constantType->getValue() === 'string') { + if ($constantStringValue === 'string') { $type = new StringType(); } - if ($constantType->getValue() === 'array') { + if ($constantStringValue === 'array') { $type = new ArrayType(new MixedType(), new MixedType()); } - if ($constantType->getValue() === 'boolean') { + if ($constantStringValue === 'boolean') { $type = new BooleanType(); } - if (in_array($constantType->getValue(), ['resource', 'resource (closed)'], true)) { + if (in_array($constantStringValue, ['resource', 'resource (closed)'], true)) { $type = new ResourceType(); } - if ($constantType->getValue() === 'integer') { + if ($constantStringValue === 'integer') { $type = new IntegerType(); } - if ($constantType->getValue() === 'double') { + if ($constantStringValue === 'double') { $type = new FloatType(); } - if ($constantType->getValue() === 'NULL') { + if ($constantStringValue === 'NULL') { $type = new NullType(); } - if ($constantType->getValue() === 'object') { + if ($constantStringValue === 'object') { $type = new ObjectWithoutClassType(); } if ($type !== null) { - $callType = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - $argType = $this->create($exprNode->getArgs()[0]->value, $type, $context, false, $scope, $rootExpr); + $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); } } @@ -1260,7 +1271,7 @@ private function specifyTypesForConstantStringBinaryExpression( && isset($exprNode->getArgs()[0]) ) { $argType = $scope->getType($exprNode->getArgs()[0]->value); - $objectType = new ObjectType($constantType->getValue()); + $objectType = new ObjectType($constantStringValue); $classStringType = new GenericClassStringType($objectType); if ($argType->isString()->yes()) { @@ -1268,9 +1279,8 @@ private function specifyTypesForConstantStringBinaryExpression( $exprNode->getArgs()[0]->value, $classStringType, $context, - false, $scope, - ); + )->setRootExpr($rootExpr); } if ($argType->isObject()->yes()) { @@ -1278,37 +1288,35 @@ private function specifyTypesForConstantStringBinaryExpression( $exprNode->getArgs()[0]->value, $objectType, $context, - false, $scope, - ); + )->setRootExpr($rootExpr); } return $this->create( $exprNode->getArgs()[0]->value, TypeCombinator::union($objectType, $classStringType), $context, - false, $scope, - ); + )->setRootExpr($rootExpr); } return null; } - private function handleDefaultTruthyOrFalseyContext(TypeSpecifierContext $context, ?Expr $rootExpr, Expr $expr, Scope $scope): SpecifiedTypes + private function handleDefaultTruthyOrFalseyContext(TypeSpecifierContext $context, Expr $expr, Scope $scope): SpecifiedTypes { if ($context->null()) { - return new SpecifiedTypes([], [], false, [], $rootExpr); + return (new SpecifiedTypes([], []))->setRootExpr($expr); } if (!$context->truthy()) { $type = StaticTypeFactory::truthy(); - return $this->create($expr, $type, TypeSpecifierContext::createFalse(), false, $scope, $rootExpr); + 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, $rootExpr); + return $this->create($expr, $type, TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr); } - return new SpecifiedTypes([], [], false, [], $rootExpr); + return (new SpecifiedTypes([], []))->setRootExpr($expr); } private function specifyTypesFromConditionalReturnType( @@ -1393,7 +1401,6 @@ public function getConditionalSpecifiedTypes( $argsMap[$parameterName], $targetType, $context, - false, $scope, ); @@ -1489,10 +1496,8 @@ static function (Type $type, callable $traverse) use ($templateTypeMap, &$contai $assertExpr, $assertedType, $assert->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue(), - false, $scope, - $containsUnresolvedTemplate || $assert->isEquality() ? $call : null, - ); + )->setRootExpr($containsUnresolvedTemplate || $assert->isEquality() ? $call : null); $types = $types !== null ? $types->unionWith($newTypes) : $newTypes; if (!$context->null() || !$assertedType instanceof ConstantBooleanType) { @@ -1650,7 +1655,7 @@ private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTy } /** - * @return array{Expr, ConstantScalarType}|null + * @return array{Expr, ConstantScalarType, Type}|null */ private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array { @@ -1670,15 +1675,13 @@ private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\ if ( $leftType instanceof ConstantScalarType && !$rightExpr instanceof ConstFetch - && !$rightExpr instanceof ClassConstFetch ) { - return [$binaryOperation->right, $leftType]; + return [$binaryOperation->right, $leftType, $rightType]; } elseif ( $rightType instanceof ConstantScalarType && !$leftExpr instanceof ConstFetch - && !$leftExpr instanceof ClassConstFetch ) { - return [$binaryOperation->left, $rightType]; + return [$binaryOperation->left, $rightType, $leftType]; } return null; @@ -1689,13 +1692,11 @@ public function create( Expr $expr, Type $type, TypeSpecifierContext $context, - bool $overwrite = false, - ?Scope $scope = null, - ?Expr $rootExpr = null, + Scope $scope, ): SpecifiedTypes { if ($expr instanceof Instanceof_ || $expr instanceof Expr\List_) { - return new SpecifiedTypes([], [], false, [], $rootExpr); + return (new SpecifiedTypes([], []))->setRootExpr($expr); } $specifiedExprs = []; @@ -1721,7 +1722,7 @@ public function create( $types = null; foreach ($specifiedExprs as $specifiedExpr) { - $newTypes = $this->createForExpr($specifiedExpr, $type, $context, $overwrite, $scope, $rootExpr); + $newTypes = $this->createForExpr($specifiedExpr, $type, $context, $scope); if ($types === null) { $types = $newTypes; @@ -1737,17 +1738,13 @@ private function createForExpr( Expr $expr, Type $type, TypeSpecifierContext $context, - bool $overwrite = false, - ?Scope $scope = null, - ?Expr $rootExpr = null, + Scope $scope, ): SpecifiedTypes { - if ($scope !== null) { - if ($context->true()) { - $containsNull = !$type->isNull()->no() && !$scope->getType($expr)->isNull()->no(); - } elseif ($context->false()) { - $containsNull = !TypeCombinator::containsNull($type) && !$scope->getType($expr)->isNull()->no(); - } + if ($context->true()) { + $containsNull = !$type->isNull()->no() && !$scope->getType($expr)->isNull()->no(); + } elseif ($context->false()) { + $containsNull = !TypeCombinator::containsNull($type) && !$scope->getType($expr)->isNull()->no(); } $originalExpr = $expr; @@ -1756,8 +1753,7 @@ private function createForExpr( } if ( - $scope !== null - && !$context->null() + !$context->null() && $expr instanceof Expr\BinaryOp\Coalesce ) { $rightIsSuperType = $type->isSuperTypeOf($scope->getType($expr->right)); @@ -1773,24 +1769,23 @@ private function createForExpr( $has = $this->reflectionProvider->hasFunction($expr->name, $scope); if (!$has) { // backwards compatibility with previous behaviour - return new SpecifiedTypes([], [], false, [], $rootExpr); + return new SpecifiedTypes([], []); } $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); $hasSideEffects = $functionReflection->hasSideEffects(); if ($hasSideEffects->yes()) { - return new SpecifiedTypes([], [], false, [], $rootExpr); + return new SpecifiedTypes([], []); } if (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no()) { - return new SpecifiedTypes([], [], false, [], $rootExpr); + return new SpecifiedTypes([], []); } } if ( $expr instanceof MethodCall && $expr->name instanceof Node\Identifier - && $scope !== null ) { $methodName = $expr->name->toString(); $calledOnType = $scope->getType($expr->var); @@ -1801,17 +1796,16 @@ private function createForExpr( || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no()) ) { if (isset($containsNull) && !$containsNull) { - return $this->createNullsafeTypes($rootExpr, $originalExpr, $scope, $context, $overwrite, $type); + return $this->createNullsafeTypes($originalExpr, $scope, $context, $type); } - return new SpecifiedTypes([], [], false, [], $rootExpr); + return new SpecifiedTypes([], []); } } if ( $expr instanceof StaticCall && $expr->name instanceof Node\Identifier - && $scope !== null ) { $methodName = $expr->name->toString(); if ($expr->class instanceof Name) { @@ -1827,10 +1821,10 @@ private function createForExpr( || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no()) ) { if (isset($containsNull) && !$containsNull) { - return $this->createNullsafeTypes($rootExpr, $originalExpr, $scope, $context, $overwrite, $type); + return $this->createNullsafeTypes($originalExpr, $scope, $context, $type); } - return new SpecifiedTypes([], [], false, [], $rootExpr); + return new SpecifiedTypes([], []); } } @@ -1850,61 +1844,61 @@ private function createForExpr( } } - $types = new SpecifiedTypes($sureTypes, $sureNotTypes, $overwrite, [], $rootExpr); - if ($scope !== null && isset($containsNull) && !$containsNull) { - return $this->createNullsafeTypes($rootExpr, $originalExpr, $scope, $context, $overwrite, $type)->unionWith($types); + $types = new SpecifiedTypes($sureTypes, $sureNotTypes); + if (isset($containsNull) && !$containsNull) { + return $this->createNullsafeTypes($originalExpr, $scope, $context, $type)->unionWith($types); } return $types; } - private function createNullsafeTypes(?Expr $rootExpr, Expr $expr, Scope $scope, TypeSpecifierContext $context, bool $overwrite, ?Type $type): SpecifiedTypes + private function createNullsafeTypes(Expr $expr, Scope $scope, TypeSpecifierContext $context, ?Type $type): SpecifiedTypes { if ($expr instanceof Expr\NullsafePropertyFetch) { if ($type !== null) { - $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), $type, $context, false, $scope, $rootExpr); + $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, $rootExpr); + $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(), $overwrite, $scope, $rootExpr), + $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, $overwrite, $scope, $rootExpr); + $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(), $overwrite, $scope, $rootExpr); + $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(), $overwrite, $scope, $rootExpr), + $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), $scope), ); } if ($expr instanceof Expr\PropertyFetch) { - return $this->createNullsafeTypes($rootExpr, $expr->var, $scope, $context, $overwrite, null); + return $this->createNullsafeTypes($expr->var, $scope, $context, null); } if ($expr instanceof Expr\MethodCall) { - return $this->createNullsafeTypes($rootExpr, $expr->var, $scope, $context, $overwrite, null); + return $this->createNullsafeTypes($expr->var, $scope, $context, null); } if ($expr instanceof Expr\ArrayDimFetch) { - return $this->createNullsafeTypes($rootExpr, $expr->var, $scope, $context, $overwrite, null); + return $this->createNullsafeTypes($expr->var, $scope, $context, null); } if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) { - return $this->createNullsafeTypes($rootExpr, $expr->class, $scope, $context, $overwrite, null); + return $this->createNullsafeTypes($expr->class, $scope, $context, null); } if ($expr instanceof Expr\StaticCall && $expr->class instanceof Expr) { - return $this->createNullsafeTypes($rootExpr, $expr->class, $scope, $context, $overwrite, null); + return $this->createNullsafeTypes($expr->class, $scope, $context, null); } - return new SpecifiedTypes([], [], $overwrite, [], $rootExpr); + return new SpecifiedTypes([], []); } private function createRangeTypes(?Expr $rootExpr, Expr $expr, Type $type, TypeSpecifierContext $context): SpecifiedTypes @@ -1921,7 +1915,7 @@ private function createRangeTypes(?Expr $rootExpr, Expr $expr, Type $type, TypeS } } - return new SpecifiedTypes([], $sureNotTypes, false, [], $rootExpr); + return (new SpecifiedTypes(sureNotTypes: $sureNotTypes))->setRootExpr($rootExpr); } /** @@ -1983,19 +1977,32 @@ private function getTypeSpecifyingExtensionsForType(array $extensions, string $c return array_merge(...$extensionsForClass); } - public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecifierContext $context, ?Expr $rootExpr): SpecifiedTypes + 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]; - if (!$context->null() && ($constantType->getValue() === false || $constantType->getValue() === null)) { + $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(), - $rootExpr, - ); + )->setRootExpr($expr); } if (!$context->null() && $constantType->getValue() === true) { @@ -2003,8 +2010,53 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif $scope, $exprNode, $context->true() ? TypeSpecifierContext::createTruthy() : TypeSpecifierContext::createTruthy()->negate(), - $rootExpr, - ); + )->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 ( @@ -2014,7 +2066,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif && isset($exprNode->getArgs()[0]) && $constantType->isString()->yes() ) { - return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context, $rootExpr); + return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context)->setRootExpr($expr); } if ( @@ -2024,7 +2076,17 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif && $exprNode->name->toLowerString() === 'preg_match' && (new ConstantIntegerType(1))->isSuperTypeOf($constantType)->yes() ) { - return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context, $rootExpr); + 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); } } @@ -2040,8 +2102,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif $expr->right, ), $context, - $rootExpr, - ); + )->setRootExpr($expr); } $rightBooleanType = $rightType->toBoolean(); @@ -2053,8 +2114,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif new ConstFetch(new Name($rightBooleanType->getValue() ? 'true' : 'false')), ), $context, - $rootExpr, - ); + )->setRootExpr($expr); } if ( @@ -2062,7 +2122,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif && $rightType->isArray()->yes() && $leftType->isConstantArray()->yes() && $leftType->isIterableAtLeastOnce()->no() ) { - return $this->create($expr->right, new NonEmptyArrayType(), $context->negate(), false, $scope, $rootExpr); + return $this->create($expr->right, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr); } if ( @@ -2070,7 +2130,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif && $leftType->isArray()->yes() && $rightType->isConstantArray()->yes() && $rightType->isIterableAtLeastOnce()->no() ) { - return $this->create($expr->left, new NonEmptyArrayType(), $context->negate(), false, $scope, $rootExpr); + return $this->create($expr->left, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr); } if ( @@ -2079,32 +2139,34 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif || ($leftType->isFloat()->yes() && $rightType->isFloat()->yes()) || ($leftType->isEnum()->yes() && $rightType->isEnum()->yes()) ) { - return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context, $rootExpr); + 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([], [], false, [], $rootExpr); + return (new SpecifiedTypes([], []))->setRootExpr($expr); } } - $leftTypes = $this->create($expr->left, $leftType, $context, false, $scope, $rootExpr); - $rightTypes = $this->create($expr->right, $rightType, $context, false, $scope, $rootExpr); + $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, ?Expr $rootExpr): SpecifiedTypes + 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(); @@ -2113,8 +2175,96 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty 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 @@ -2126,10 +2276,10 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty $scope, $leftExpr, $context, - $rootExpr, - ); + )->setRootExpr($expr); } + // get_class($a) === 'Foo' if ( $context->true() && $unwrappedLeftExpr instanceof FuncCall @@ -2137,22 +2287,62 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty && 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, - false, $scope, - $rootExpr, - )->unionWith($this->create($leftExpr, $rightType, $context, false, $scope, $rootExpr)); + )->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 (count($rightType->getConstantStrings()) > 0) { + if ($rightType->isString()->yes()) { $types = null; foreach ($rightType->getConstantStrings() as $constantString) { - $specifiedType = $this->specifyTypesForConstantStringBinaryExpression($unwrappedLeftExpr, $constantString, $context, $scope, $rootExpr); + $specifiedType = $this->specifyTypesForConstantStringBinaryExpression($unwrappedLeftExpr, $constantString, $context, $scope, $expr); + if ($specifiedType === null) { continue; } @@ -2166,7 +2356,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty if ($types !== null) { if ($leftExpr !== $unwrappedLeftExpr) { - $types = $types->unionWith($this->create($leftExpr, $rightType, $context, false, $scope, $rootExpr)); + $types = $types->unionWith($this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr)); } return $types; } @@ -2182,17 +2372,18 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty $unwrappedExprNode = $exprNode->getExpr(); } - $specifiedType = $this->specifyTypesForConstantBinaryExpression($unwrappedExprNode, $constantType, $context, $scope, $rootExpr); + $specifiedType = $this->specifyTypesForConstantBinaryExpression($unwrappedExprNode, $constantType, $context, $scope, $expr); if ($specifiedType !== null) { if ($exprNode !== $unwrappedExprNode) { $specifiedType = $specifiedType->unionWith( - $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr), + $this->create($exprNode, $constantType, $context, $scope)->setRootExpr($expr), ); } return $specifiedType; } } + // $a::class === 'Foo' if ( $context->true() && $unwrappedLeftExpr instanceof ClassConstFetch && @@ -2203,6 +2394,14 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty $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_( @@ -2210,11 +2409,12 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty new Name($rightType->getValue()), ), $context, - $rootExpr, - )->unionWith($this->create($leftExpr, $rightType, $context, false, $scope, $rootExpr)); + )->unionWith($this->create($leftExpr, $rightType, $context, $scope))->setRootExpr($expr); } $leftType = $scope->getType($leftExpr); + + // 'Foo' === $a::class if ( $context->true() && $unwrappedRightExpr instanceof ClassConstFetch && @@ -2225,6 +2425,15 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty $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_( @@ -2232,8 +2441,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty new Name($leftType->getValue()), ), $context, - $rootExpr, - )->unionWith($this->create($rightExpr, $leftType, $context, false, $scope, $rootExpr)); + )->unionWith($this->create($rightExpr, $leftType, $context, $scope)->setRootExpr($expr)); } if ($context->false()) { @@ -2241,16 +2449,16 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty if ($identicalType instanceof ConstantBooleanType) { $never = new NeverType(); $contextForTypes = $identicalType->getValue() ? $context->negate() : $context; - $leftTypes = $this->create($leftExpr, $never, $contextForTypes, false, $scope, $rootExpr); - $rightTypes = $this->create($rightExpr, $never, $contextForTypes, false, $scope, $rootExpr); + $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, false, $scope, $rootExpr), + $this->create($unwrappedLeftExpr, $never, $contextForTypes, $scope)->setRootExpr($expr), ); } if ($rightExpr instanceof AlwaysRememberedExpr) { $rightTypes = $rightTypes->unionWith( - $this->create($unwrappedRightExpr, $never, $contextForTypes, false, $scope, $rootExpr), + $this->create($unwrappedRightExpr, $never, $contextForTypes, $scope)->setRootExpr($expr), ); } return $leftTypes->unionWith($rightTypes); @@ -2260,48 +2468,49 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty $types = null; if ( count($leftType->getFiniteTypes()) === 1 - || ($context->true() && $leftType->isConstantValue()->yes() && !$rightType->equals($leftType) && $rightType->isSuperTypeOf($leftType)->yes()) + || ( + $context->true() + && $leftType->isConstantValue()->yes() + && !$rightType->equals($leftType) + && $rightType->isSuperTypeOf($leftType)->yes()) ) { $types = $this->create( $rightExpr, $leftType, $context, - false, $scope, - $rootExpr, - ); + )->setRootExpr($expr); if ($rightExpr instanceof AlwaysRememberedExpr) { $types = $types->unionWith($this->create( $unwrappedRightExpr, $leftType, $context, - false, $scope, - $rootExpr, - )); + ))->setRootExpr($expr); } } if ( count($rightType->getFiniteTypes()) === 1 - || ($context->true() && $rightType->isConstantValue()->yes() && !$leftType->equals($rightType) && $leftType->isSuperTypeOf($rightType)->yes()) + || ( + $context->true() + && $rightType->isConstantValue()->yes() + && !$leftType->equals($rightType) + && $leftType->isSuperTypeOf($rightType)->yes() + ) ) { $leftTypes = $this->create( $leftExpr, $rightType, $context, - false, $scope, - $rootExpr, - ); + )->setRootExpr($expr); if ($leftExpr instanceof AlwaysRememberedExpr) { $leftTypes = $leftTypes->unionWith($this->create( $unwrappedLeftExpr, $rightType, $context, - false, $scope, - $rootExpr, - )); + ))->setRootExpr($expr); } if ($types !== null) { $types = $types->unionWith($leftTypes); @@ -2318,30 +2527,30 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty $rightExprString = $this->exprPrinter->printExpr($unwrappedRightExpr); if ($leftExprString === $rightExprString) { if (!$unwrappedLeftExpr instanceof Expr\Variable || !$unwrappedRightExpr instanceof Expr\Variable) { - return new SpecifiedTypes([], [], false, [], $rootExpr); + return (new SpecifiedTypes([], []))->setRootExpr($expr); } } if ($context->true()) { - $leftTypes = $this->create($leftExpr, $rightType, $context, false, $scope, $rootExpr); - $rightTypes = $this->create($rightExpr, $leftType, $context, false, $scope, $rootExpr); + $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, false, $scope, $rootExpr), + $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr), ); } if ($rightExpr instanceof AlwaysRememberedExpr) { $rightTypes = $rightTypes->unionWith( - $this->create($unwrappedRightExpr, $leftType, $context, false, $scope, $rootExpr), + $this->create($unwrappedRightExpr, $leftType, $context, $scope)->setRootExpr($expr), ); } return $leftTypes->unionWith($rightTypes); } elseif ($context->false()) { - return $this->create($leftExpr, $leftType, $context, false, $scope, $rootExpr)->normalize($scope) - ->intersectWith($this->create($rightExpr, $rightType, $context, false, $scope, $rootExpr)->normalize($scope)); + return $this->create($leftExpr, $leftType, $context, $scope)->setRootExpr($expr)->normalize($scope) + ->intersectWith($this->create($rightExpr, $rightType, $context, $scope)->setRootExpr($expr)->normalize($scope)); } - return new SpecifiedTypes([], [], false, [], $rootExpr); + return (new SpecifiedTypes([], []))->setRootExpr($expr); } } diff --git a/src/Analyser/TypeSpecifierContext.php b/src/Analyser/TypeSpecifierContext.php index a14aba7112..fe09aa861c 100644 --- a/src/Analyser/TypeSpecifierContext.php +++ b/src/Analyser/TypeSpecifierContext.php @@ -4,8 +4,10 @@ use PHPStan\ShouldNotHappenException; -/** @api */ -class TypeSpecifierContext +/** + * @api + */ +final class TypeSpecifierContext { public const CONTEXT_TRUE = 0b0001; diff --git a/src/Analyser/TypeSpecifierFactory.php b/src/Analyser/TypeSpecifierFactory.php index eddf559900..5df7f61e07 100644 --- a/src/Analyser/TypeSpecifierFactory.php +++ b/src/Analyser/TypeSpecifierFactory.php @@ -3,12 +3,15 @@ namespace PHPStan\Analyser; 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'; @@ -24,6 +27,7 @@ public function create(): TypeSpecifier $typeSpecifier = new TypeSpecifier( $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), diff --git a/src/Analyser/UndefinedVariableException.php b/src/Analyser/UndefinedVariableException.php index 6719de349c..a6e805d69a 100644 --- a/src/Analyser/UndefinedVariableException.php +++ b/src/Analyser/UndefinedVariableException.php @@ -5,7 +5,13 @@ use PHPStan\AnalysedCodeException; use function sprintf; -class UndefinedVariableException extends AnalysedCodeException +/** + * @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 { public function __construct(private Scope $scope, private string $variableName) diff --git a/src/Broker/AnonymousClassNameHelper.php b/src/Broker/AnonymousClassNameHelper.php index a09a15e760..5ca264ab16 100644 --- a/src/Broker/AnonymousClassNameHelper.php +++ b/src/Broker/AnonymousClassNameHelper.php @@ -3,6 +3,8 @@ 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; @@ -10,16 +12,21 @@ use function md5; use function sprintf; -class AnonymousClassNameHelper +#[AutowiredService] +final class AnonymousClassNameHelper { public function __construct( private FileHelper $fileHelper, + #[AutowiredParameter(ref: '@simpleRelativePathHelper')] private RelativePathHelper $relativePathHelper, ) { } + /** + * @return non-empty-string + */ public function getAnonymousClassName( Node\Stmt\Class_ $classNode, string $filename, diff --git a/src/Broker/Broker.php b/src/Broker/Broker.php deleted file mode 100644 index fee5eab88d..0000000000 --- a/src/Broker/Broker.php +++ /dev/null @@ -1,144 +0,0 @@ -reflectionProvider->hasClass($className); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function getClass(string $className): ClassReflection - { - return $this->reflectionProvider->getClass($className); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function getClassName(string $className): string - { - return $this->reflectionProvider->getClassName($className); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function supportsAnonymousClasses(): bool - { - return $this->reflectionProvider->supportsAnonymousClasses(); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection - { - return $this->reflectionProvider->getAnonymousClassReflection($classNode, $scope); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function hasFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool - { - return $this->reflectionProvider->hasFunction($nameNode, $namespaceAnswerer); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function getFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): FunctionReflection - { - return $this->reflectionProvider->getFunction($nameNode, $namespaceAnswerer); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function resolveFunctionName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string - { - return $this->reflectionProvider->resolveFunctionName($nameNode, $namespaceAnswerer); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function hasConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool - { - return $this->reflectionProvider->hasConstant($nameNode, $namespaceAnswerer); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): GlobalConstantReflection - { - return $this->reflectionProvider->getConstant($nameNode, $namespaceAnswerer); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function resolveConstantName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string - { - return $this->reflectionProvider->resolveConstantName($nameNode, $namespaceAnswerer); - } - - /** - * @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 d35b68e30d..bbd8d97a3d 100644 --- a/src/Broker/BrokerFactory.php +++ b/src/Broker/BrokerFactory.php @@ -2,10 +2,7 @@ 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'; @@ -17,16 +14,4 @@ class BrokerFactory public const OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG = 'phpstan.broker.operatorTypeSpecifyingExtension'; public const EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG = 'phpstan.broker.expressionTypeResolverExtension'; - public function __construct(private Container $container) - { - } - - public function create(): Broker - { - return new Broker( - $this->container->getByType(ReflectionProvider::class), - $this->container->getParameter('universalObjectCratesClasses'), - ); - } - } diff --git a/src/Broker/ClassAutoloadingException.php b/src/Broker/ClassAutoloadingException.php deleted file mode 100644 index 23e99fe5a5..0000000000 --- a/src/Broker/ClassAutoloadingException.php +++ /dev/null @@ -1,47 +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 d86a1bcb08..0cabab122c 100644 --- a/src/Broker/ClassNotFoundException.php +++ b/src/Broker/ClassNotFoundException.php @@ -5,7 +5,14 @@ use PHPStan\AnalysedCodeException; use function sprintf; -class ClassNotFoundException extends AnalysedCodeException +/** + * @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(private string $className) @@ -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 2c490e3653..41981f07d8 100644 --- a/src/Broker/ConstantNotFoundException.php +++ b/src/Broker/ConstantNotFoundException.php @@ -5,7 +5,14 @@ use PHPStan\AnalysedCodeException; use function sprintf; -class ConstantNotFoundException extends AnalysedCodeException +/** + * @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(private string $constantName) @@ -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 de2728001f..9607608462 100644 --- a/src/Broker/FunctionNotFoundException.php +++ b/src/Broker/FunctionNotFoundException.php @@ -5,7 +5,14 @@ use PHPStan\AnalysedCodeException; use function sprintf; -class FunctionNotFoundException extends AnalysedCodeException +/** + * @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(private string $functionName) @@ -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 0180ab6610..8f4deb6f13 100644 --- a/src/Cache/Cache.php +++ b/src/Cache/Cache.php @@ -2,10 +2,17 @@ namespace PHPStan\Cache; -class Cache +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; + +#[AutowiredService] +final class Cache { - public function __construct(private CacheStorage $storage) + public function __construct( + #[AutowiredParameter(ref: '@cacheStorage')] + private CacheStorage $storage, + ) { } diff --git a/src/Cache/CacheItem.php b/src/Cache/CacheItem.php index 61d9946c90..101bbfe2fd 100644 --- a/src/Cache/CacheItem.php +++ b/src/Cache/CacheItem.php @@ -2,7 +2,7 @@ namespace PHPStan\Cache; -class CacheItem +final class CacheItem { /** diff --git a/src/Cache/FileCacheStorage.php b/src/Cache/FileCacheStorage.php index 38b36d25aa..1b66f26e2a 100644 --- a/src/Cache/FileCacheStorage.php +++ b/src/Cache/FileCacheStorage.php @@ -4,23 +4,40 @@ use InvalidArgumentException; use Nette\Utils\Random; +use PHPStan\File\CouldNotReadFileException; +use PHPStan\File\CouldNotWriteFileException; +use PHPStan\File\FileReader; use PHPStan\File\FileWriter; use PHPStan\Internal\DirectoryCreator; use PHPStan\Internal\DirectoryCreatorException; use PHPStan\ShouldNotHappenException; +use 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; -class FileCacheStorage implements CacheStorage +final class FileCacheStorage implements CacheStorage { + private const CACHED_CLEARED_VERSION = 'v2-new'; + public function __construct(private string $directory) { } @@ -100,4 +117,90 @@ private function getFilePaths(string $key): array ]; } + public function clearUnusedFiles(): void + { + if (!is_dir($this->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 158c285031..324dfcf37f 100644 --- a/src/Cache/MemoryCacheStorage.php +++ b/src/Cache/MemoryCacheStorage.php @@ -4,7 +4,7 @@ use function var_export; -class MemoryCacheStorage implements CacheStorage +final class MemoryCacheStorage implements CacheStorage { /** @var array */ diff --git a/src/Collectors/CollectedData.php b/src/Collectors/CollectedData.php index 76d7bb5fe4..ae13a94614 100644 --- a/src/Collectors/CollectedData.php +++ b/src/Collectors/CollectedData.php @@ -3,11 +3,16 @@ namespace PHPStan\Collectors; use JsonSerializable; +use Override; use PhpParser\Node; use ReturnTypeWillChange; -/** @api */ -class CollectedData implements JsonSerializable +/** + * @api + * + * @phpstan-type CollectorData = array>, list>> + */ +final class CollectedData implements JsonSerializable { /** @@ -49,6 +54,7 @@ public function getCollectorType(): string * @return mixed */ #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ diff --git a/src/Collectors/Collector.php b/src/Collectors/Collector.php index e046f89750..d7c87c8ecc 100644 --- a/src/Collectors/Collector.php +++ b/src/Collectors/Collector.php @@ -20,19 +20,19 @@ * Learn more: https://phpstan.org/developing-extensions/collectors * * @api - * @phpstan-template-covariant TNodeType of Node - * @phpstan-template-covariant TValue + * @template-covariant TNodeType of Node + * @template-covariant TValue */ interface Collector { /** - * @phpstan-return class-string + * @return class-string */ public function getNodeType(): string; /** - * @phpstan-param TNodeType $node + * @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 index 65b80118b2..c0ff3dd404 100644 --- a/src/Collectors/Registry.php +++ b/src/Collectors/Registry.php @@ -3,10 +3,12 @@ namespace PHPStan\Collectors; use PhpParser\Node; +use PHPStan\DependencyInjection\AutowiredService; use function class_implements; use function class_parents; -class Registry +#[AutowiredService(factory: '@PHPStan\Collectors\RegistryFactory::create')] +final class Registry { /** @var Collector[][] */ @@ -27,10 +29,8 @@ public function __construct(array $collectors) /** * @template TNodeType of Node - * @phpstan-param class-string $nodeType - * @param Node $nodeType - * @phpstan-return array> - * @return Collector[] + * @param class-string $nodeType + * @return array> */ public function getCollectors(string $nodeType): array { @@ -48,8 +48,7 @@ public function getCollectors(string $nodeType): array } /** - * @phpstan-var array> $selectedCollectors - * @var Collector[] $selectedCollectors + * @var array> $selectedCollectors */ $selectedCollectors = $this->cache[$nodeType]; diff --git a/src/Collectors/RegistryFactory.php b/src/Collectors/RegistryFactory.php index 0f852d3210..a95dbe2a51 100644 --- a/src/Collectors/RegistryFactory.php +++ b/src/Collectors/RegistryFactory.php @@ -2,9 +2,11 @@ namespace PHPStan\Collectors; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; -class RegistryFactory +#[AutowiredService] +final class RegistryFactory { public const COLLECTOR_TAG = 'phpstan.collector'; diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index 85acf2d88c..53168f8edd 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -4,8 +4,12 @@ use PHPStan\Analyser\AnalyserResult; 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; @@ -19,7 +23,12 @@ 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 { public function __construct( @@ -46,11 +55,17 @@ public function analyse( bool $debug, ?string $projectConfigFile, ?array $projectConfigArray, + ?string $tmpFile, + ?string $insteadOfFile, InputInterface $input, ): AnalysisResult { $isResultCacheUsed = false; - $resultCacheManager = $this->resultCacheManagerFactory->create(); + $fileReplacements = []; + if ($tmpFile !== null && $insteadOfFile !== null) { + $fileReplacements = [$insteadOfFile => $tmpFile]; + } + $resultCacheManager = $this->resultCacheManagerFactory->create($fileReplacements); $ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize(); $fileSpecificErrors = []; @@ -60,7 +75,7 @@ public function analyse( $collectedData = []; $savedResultCache = false; $memoryUsageBytes = memory_get_peak_usage(true); - if ($errorOutput->isDebug()) { + if ($errorOutput->isVeryVerbose()) { $errorOutput->writeLineFormatted('Result cache was not saved because of ignoredErrorHelperResult errors.'); } $changedProjectExtensionFilesOutsideOfAnalysedPaths = []; @@ -71,6 +86,8 @@ public function analyse( $files, $debug, $projectConfigFile, + $tmpFile, + $insteadOfFile, $stdOutput, $errorOutput, $input, @@ -95,6 +112,7 @@ public function analyse( $intermediateAnalyserResult->getInternalErrors(), $intermediateAnalyserResult->getCollectedData(), $intermediateAnalyserResult->getDependencies(), + $intermediateAnalyserResult->getUsedTraitDependencies(), $intermediateAnalyserResult->getExportedNodes(), $intermediateAnalyserResult->hasReachedInternalErrorsCountLimit(), $intermediateAnalyserResult->getPeakMemoryUsageBytes(), @@ -102,7 +120,11 @@ public function analyse( } $resultCacheResult = $resultCacheManager->process($intermediateAnalyserResult, $resultCache, $errorOutput, $onlyFiles, true); - $analyserResult = $this->analyserResultFinalizer->finalize($resultCacheResult->getAnalyserResult(), $onlyFiles, $debug)->getAnalyserResult(); + $analyserResult = $this->analyserResultFinalizer->finalize( + $this->switchTmpFileInAnalyserResult($resultCacheResult->getAnalyserResult(), $insteadOfFile, $tmpFile), + $onlyFiles, + $debug, + )->getAnalyserResult(); $internalErrors = $analyserResult->getInternalErrors(); $errors = array_merge( $analyserResult->getErrors(), @@ -150,7 +172,7 @@ public function analyse( $notFileSpecificErrors, $internalErrors, [], - $collectedData, + $this->mapCollectedData($collectedData), $defaultLevelUsed, $projectConfigFile, $savedResultCache, @@ -160,6 +182,22 @@ public function analyse( ); } + /** + * @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 @@ -169,6 +207,8 @@ private function runAnalyser( array $allAnalysedFiles, bool $debug, ?string $projectConfigFile, + ?string $tmpFile, + ?string $insteadOfFile, Output $stdOutput, Output $errorOutput, InputInterface $input, @@ -180,7 +220,7 @@ private function runAnalyser( $errorOutput->getStyle()->progressStart($allAnalysedFilesCount); $errorOutput->getStyle()->progressAdvance($allAnalysedFilesCount); $errorOutput->getStyle()->progressFinish(); - return new AnalyserResult([], [], [], [], [], [], [], [], [], [], false, memory_get_peak_usage(true)); + return new AnalyserResult([], [], [], [], [], [], [], [], [], [], [], false, memory_get_peak_usage(true)); } if (!$debug) { @@ -212,7 +252,7 @@ private function runAnalyser( } } - $analyserResult = $this->analyserRunner->runAnalyser($files, $allAnalysedFiles, $preFileCallback, $postFileCallback, $debug, true, $projectConfigFile, $input); + $analyserResult = $this->analyserRunner->runAnalyser($files, $allAnalysedFiles, $preFileCallback, $postFileCallback, $debug, true, $projectConfigFile, $tmpFile, $insteadOfFile, $input); if (!$debug) { $errorOutput->getStyle()->progressFinish(); @@ -221,4 +261,135 @@ private function runAnalyser( return $analyserResult; } + 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 + { + $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 6ecfba9105..926aa66408 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -3,6 +3,7 @@ namespace PHPStan\Command; use OndraM\CiDetector\CiDetector; +use Override; use PHPStan\Analyser\InternalError; use PHPStan\Command\ErrorFormatter\BaselineNeonErrorFormatter; use PHPStan\Command\ErrorFormatter\BaselinePhpErrorFormatter; @@ -13,11 +14,15 @@ 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; @@ -34,6 +39,7 @@ 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; @@ -50,13 +56,17 @@ use function pathinfo; use function rewind; use function sprintf; +use function str_contains; use function stream_get_contents; use function strlen; use function substr; use const PATHINFO_BASENAME; use const PATHINFO_EXTENSION; -class AnalyseCommand extends Command +/** + * @phpstan-import-type Trace from InternalError as InternalErrorTrace + */ +final class AnalyseCommand extends Command { private const NAME = 'analyse'; @@ -76,6 +86,7 @@ public function __construct( parent::__construct(); } + #[Override] protected function configure(): void { $this->setName(self::NAME) @@ -84,29 +95,33 @@ protected function configure(): void new InputArgument('paths', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Paths with source code to run analysis on'), new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to project configuration file'), new InputOption(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('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', null, InputOption::VALUE_NONE, 'Do not error out when the generated baseline is empty'), - new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for analysis'), - new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with Xdebug for debugging purposes'), - new InputOption('fix', null, InputOption::VALUE_NONE, 'Launch PHPStan Pro'), - new InputOption('watch', null, InputOption::VALUE_NONE, 'Launch PHPStan Pro'), - new InputOption('pro', null, InputOption::VALUE_NONE, 'Launch PHPStan Pro'), - new InputOption('fail-without-result-cache', null, InputOption::VALUE_NONE, 'Return non-zero exit code when result cache is not used'), + 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')) { @@ -119,6 +134,7 @@ protected function initialize(InputInterface $input, OutputInterface $output): v } } + #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { $paths = $input->getArgument('paths'); @@ -128,7 +144,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $level = $input->getOption(self::OPTION_LEVEL); $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 */ @@ -141,12 +158,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int $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($tmpFile) && $tmpFile !== null) + || (!is_string($insteadOfFile) && $insteadOfFile !== null) || (!is_bool($allowXdebug)) ) { throw new ShouldNotHappenException(); @@ -165,6 +187,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $level, $allowXdebug, $debugEnabled, + $tmpFile, + $insteadOfFile, + true, ); } catch (InceptionNotSuccessfulException $e) { return 1; @@ -175,16 +200,31 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); } - $errorOutput = $inceptionResult->getErrorOutput(); - $obsoleteDockerImage = $_SERVER['PHPSTAN_OBSOLETE_DOCKER_IMAGE'] ?? 'false'; - if ($obsoleteDockerImage === 'true') { - $errorOutput->writeLineFormatted('⚠️ You\'re using an obsolete PHPStan Docker image. ⚠️️'); - $errorOutput->writeLineFormatted(' You can obtain the current one from ghcr.io/phpstan/phpstan.'); - $errorOutput->writeLineFormatted(' Read more about it here:'); - $errorOutput->writeLineFormatted(' https://phpstan.org/user-guide/docker'); - $errorOutput->writeLineFormatted(''); + 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) { @@ -237,22 +277,27 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if (count($files) === 0) { - $bleedingEdge = (bool) $container->getParameter('featureToggles')['zeroFiles']; - $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); - if (!$bleedingEdge) { - $inceptionResult->getErrorOutput()->getStyle()->note('No files found to analyse.'); - $inceptionResult->getErrorOutput()->getStyle()->warning('This will cause a non-zero exit code in PHPStan 2.0.'); - - return $inceptionResult->handleReturn(0, null, $this->analysisStartTime); - } - $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'); @@ -273,7 +318,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int )); } - if ($fix) { + 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); @@ -290,6 +335,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new ShouldNotHappenException(); } + if ($fix) { + $inceptionResult->getErrorOutput()->writeLineFormatted('Analysing files...'); + } + try { $analysisResult = $application->analyse( $files, @@ -300,6 +349,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $debug, $inceptionResult->getProjectConfigFile(), $inceptionResult->getProjectConfigArray(), + $inceptionResult->getEditorModeTmpFile(), + $inceptionResult->getEditorModeInsteadOfFile(), $input, ); } catch (Throwable $t) { @@ -385,7 +436,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $internalErrorsTuples = array_values($internalErrorsTuples); - $bugReportUrl = 'https://github.com/phpstan/phpstan/issues/new?template=Bug_report.yaml'; + + $fileHelper = $container->getByType(FileHelper::class); /** * Variable $internalErrors only contains non-file-specific "internal errors". @@ -396,32 +448,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int continue; } - $message = sprintf('%s while %s', $internalError->getMessage(), $internalError->getContextDescription()); - if ($internalError->getTraceAsString() !== null) { - if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { - $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"); - } - } - } - $internalErrors[] = new InternalError( - $message, + $this->getMessageFromInternalError($fileHelper, $internalError, $output->getVerbosity()), $internalError->getContextDescription(), $internalError->getTrace(), $internalError->getTraceAsString(), @@ -482,8 +510,78 @@ protected function execute(InputInterface $input, OutputInterface $output): int ); } - $exitCode = $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()); - if ($failWithoutResultCache && !$analysisResult->isResultCacheUsed()) { + if ($fix) { + $fixableErrors = []; + foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { + if ($fileSpecificError->getFixedErrorDiff() === null) { + continue; + } + + $fixableErrors[] = $fileSpecificError; + } + + $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(); + } + + if ($fixableError->getFixedErrorDiff() === null) { + throw new ShouldNotHappenException(); + } + + $diffsByFile[$fixFile][] = $fixableError->getFixedErrorDiff(); + } + + $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; + } + + FileWriter::write($file, $finalFileContents); + } + + $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 ($exitCode === 0 && $failWithoutResultCache && !$analysisResult->isResultCacheUsed()) { $exitCode = 2; } @@ -528,12 +626,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $errorOutput->writeLineFormatted(''); - $bleedingEdge = (bool) $container->getParameter('featureToggles')['projectServicesNotInAnalysedPaths']; - if ($bleedingEdge) { - return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); - } - - $errorOutput->getStyle()->warning('This will cause a non-zero exit code in PHPStan 2.0.'); + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); } } @@ -555,6 +648,77 @@ private function createStreamOutput(): StreamOutput 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()) { @@ -582,9 +746,6 @@ private function generateBaseline(string $generateBaselineFile, InceptionResult $stream = $streamOutput->getStream(); rewind($stream); $baselineContents = stream_get_contents($stream); - if ($baselineContents === false) { - throw new ShouldNotHappenException(); - } try { DirectoryCreator::ensureDirectoryExists($baselineFileDirectory, 0644); diff --git a/src/Command/AnalyserRunner.php b/src/Command/AnalyserRunner.php index da6eb42962..f6ea231e84 100644 --- a/src/Command/AnalyserRunner.php +++ b/src/Command/AnalyserRunner.php @@ -5,18 +5,23 @@ 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 { public function __construct( @@ -42,12 +47,14 @@ public function runAnalyser( bool $debug, bool $allowParallel, ?string $projectConfigFile, + ?string $tmpFile, + ?string $insteadOfFile, InputInterface $input, ): AnalyserResult { $filesCount = count($files); if ($filesCount === 0) { - return new AnalyserResult([], [], [], [], [], [], [], [], [], [], false, memory_get_peak_usage(true)); + return new AnalyserResult([], [], [], [], [], [], [], [], [], [], [], false, memory_get_peak_usage(true)); } $schedule = $this->scheduler->scheduleWork($this->cpuCoreCounter->getNumberOfCpuCores(), $files); @@ -65,7 +72,7 @@ public function runAnalyser( ) { $loop = new StreamSelectLoop(); $result = null; - $promise = $this->parallelAnalyser->analyse($loop, $schedule, $mainScript, $postFileCallback, $projectConfigFile, $input, 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; }); @@ -77,12 +84,34 @@ public function runAnalyser( } return $this->analyser->analyse( - $files, + $this->switchTmpFile($files, $insteadOfFile, $tmpFile), $preFileCallback, $postFileCallback, $debug, - $allAnalysedFiles, + $this->switchTmpFile($allAnalysedFiles, $insteadOfFile, $tmpFile), ); } + /** + * @param string[] $analysedFiles + * @return string[] + */ + private function switchTmpFile( + array $analysedFiles, + ?string $insteadOfFile, + ?string $tmpFile, + ): array + { + if ($insteadOfFile === null) { + return $analysedFiles; + } + $analysedFiles = array_values(array_filter($analysedFiles, static fn (string $file): bool => $file !== $insteadOfFile)); + + if ($tmpFile !== null) { + array_unshift($analysedFiles, $tmpFile); + } + + return $analysedFiles; + } + } diff --git a/src/Command/AnalysisResult.php b/src/Command/AnalysisResult.php index f7c4424284..1697b5f38a 100644 --- a/src/Command/AnalysisResult.php +++ b/src/Command/AnalysisResult.php @@ -5,12 +5,13 @@ use PHPStan\Analyser\Error; use PHPStan\Analyser\InternalError; use PHPStan\Collectors\CollectedData; -use function array_map; use function count; use function usort; -/** @api */ -class AnalysisResult +/** + * @api + */ +final class AnalysisResult { /** @var list sorted by their file name, line number and message */ @@ -80,15 +81,6 @@ public function getNotFileSpecificErrors(): array return $this->notFileSpecificErrors; } - /** - * @deprecated Use getInternalErrorObjects - * @return list - */ - public function getInternalErrors(): array - { - return array_map(static fn (InternalError $internalError) => $internalError->getMessage(), $this->internalErrors); - } - /** * @return list */ diff --git a/src/Command/ClearResultCacheCommand.php b/src/Command/ClearResultCacheCommand.php index 30c59100ec..1ca5f60b6e 100644 --- a/src/Command/ClearResultCacheCommand.php +++ b/src/Command/ClearResultCacheCommand.php @@ -2,6 +2,7 @@ namespace PHPStan\Command; +use Override; use PHPStan\Analyser\ResultCache\ResultCacheClearer; use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Command\Command; @@ -11,7 +12,7 @@ use function is_bool; use function is_string; -class ClearResultCacheCommand extends Command +final class ClearResultCacheCommand extends Command { private const NAME = 'clear-result-cache'; @@ -26,6 +27,7 @@ public function __construct( parent::__construct(); } + #[Override] protected function configure(): void { $this->setName(self::NAME) @@ -33,12 +35,13 @@ protected function configure(): void ->setDefinition([ new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to project configuration file'), new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), - new InputOption('debug', null, InputOption::VALUE_NONE, 'Show debug information - which file is analysed, do not catch internal errors'), - new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for clearing result cache'), - new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with Xdebug for debugging purposes'), + 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')) { @@ -51,6 +54,7 @@ protected function initialize(InputInterface $input, OutputInterface $output): v } } + #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { $autoloadFile = $input->getOption('autoload-file'); @@ -81,6 +85,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int '0', $allowXdebug, $debugEnabled, + null, + null, + true, ); } catch (InceptionNotSuccessfulException) { return 1; diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index dee834e30b..a599a83efa 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -12,7 +12,7 @@ use Nette\Schema\ValidationException; use Nette\Utils\AssertionException; use Nette\Utils\Strings; -use PHPStan\Analyser\MutatingScope; +use PHPStan\Cache\FileCacheStorage; use PHPStan\Command\Symfony\SymfonyOutput; use PHPStan\Command\Symfony\SymfonyStyle; use PHPStan\DependencyInjection\Container; @@ -21,10 +21,12 @@ use PHPStan\DependencyInjection\InvalidExcludePathsException; use PHPStan\DependencyInjection\InvalidIgnoredErrorPatternsException; use PHPStan\DependencyInjection\LoaderFactory; +use PHPStan\DependencyInjection\MissingImplementedInterfaceInServiceWithTagException; use PHPStan\ExtensionInstaller\GeneratedConfig; use PHPStan\File\FileExcluder; use PHPStan\File\FileFinder; use PHPStan\File\FileHelper; +use PHPStan\File\ParentDirectoryRelativePathHelper; use PHPStan\File\SimpleRelativePathHelper; use PHPStan\Internal\ComposerHelper; use PHPStan\Internal\DirectoryCreator; @@ -65,7 +67,7 @@ use const E_ERROR; use const PHP_VERSION_ID; -class CommandHelper +final class CommandHelper { public const DEFAULT_LEVEL = '0'; @@ -89,8 +91,10 @@ public static function begin( ?string $generateBaselineFile, ?string $level, bool $allowXdebug, - bool $debugEnabled = false, - bool $cleanupContainerCache = true, + bool $debugEnabled, + ?string $singleReflectionFile, + ?string $singleReflectionInsteadOfFile, + bool $cleanupContainerCache, ): InceptionResult { $stdOutput = new SymfonyOutput($output, new SymfonyStyle(new ErrorsConsoleStyle($input, $output))); @@ -164,7 +168,7 @@ public static function begin( $currentWorkingDirectoryFileHelper = new FileHelper($currentWorkingDirectory); $currentWorkingDirectory = $currentWorkingDirectoryFileHelper->getWorkingDirectory(); - /** @var array|false $autoloadFunctionsBefore */ + /** @var list|false $autoloadFunctionsBefore */ $autoloadFunctionsBefore = spl_autoload_functions(); if ($autoloadFile !== null) { @@ -179,7 +183,15 @@ public static function begin( })($autoloadFile); } if ($projectConfigFile === null) { - foreach (['phpstan.neon', 'phpstan.neon.dist', 'phpstan.dist.neon'] as $discoverableConfigName) { + $discoverableConfigNames = [ + '.phpstan.neon', + 'phpstan.neon', + '.phpstan.neon.dist', + 'phpstan.neon.dist', + '.phpstan.dist.neon', + 'phpstan.dist.neon', + ]; + foreach ($discoverableConfigNames as $discoverableConfigName) { $discoverableConfigFile = $currentWorkingDirectory . DIRECTORY_SEPARATOR . $discoverableConfigName; if (is_file($discoverableConfigFile)) { $projectConfigFile = $discoverableConfigFile; @@ -195,6 +207,32 @@ public static function begin( $generateBaselineFile = $currentWorkingDirectoryFileHelper->normalizePath($currentWorkingDirectoryFileHelper->absolutizePath($generateBaselineFile)); } + 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 ($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(); + } + } + + 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(); + } + + 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(); + } + } + $defaultLevelUsed = false; if ($projectConfigFile === null && $level === null) { $level = self::DEFAULT_LEVEL; @@ -204,7 +242,10 @@ public static function begin( $paths = array_map(static fn (string $path): string => $currentWorkingDirectoryFileHelper->normalizePath($currentWorkingDirectoryFileHelper->absolutizePath($path)), $paths); $analysedPathsFromConfig = []; - $containerFactory = new ContainerFactory($currentWorkingDirectory, true); + $containerFactory = new ContainerFactory($currentWorkingDirectory); + if ($cleanupContainerCache) { + $containerFactory->setJournalContainer(); + } $projectConfig = null; if ($projectConfigFile !== null) { if (!is_file($projectConfigFile)) { @@ -217,6 +258,10 @@ public static function begin( $containerFactory->getRootDirectory(), $containerFactory->getCurrentWorkingDirectory(), $generateBaselineFile, + [ + '[parameters][paths][]', + '[parameters][tmpDir]', + ], ))->createLoader(); try { @@ -344,7 +389,7 @@ public static function begin( } try { - $container = $containerFactory->create($tmpDir, $additionalConfigFiles, $paths, $composerAutoloaderProjectPaths, $analysedPathsFromConfig, $level ?? self::DEFAULT_LEVEL, $generateBaselineFile, $autoloadFile); + $container = $containerFactory->create($tmpDir, $additionalConfigFiles, $paths, $composerAutoloaderProjectPaths, $analysedPathsFromConfig, $level ?? self::DEFAULT_LEVEL, $generateBaselineFile, $autoloadFile, $singleReflectionFile, $singleReflectionInsteadOfFile); } catch (InvalidConfigurationException | AssertionException $e) { $errorOutput->writeLineFormatted('Invalid configuration:'); $errorOutput->writeLineFormatted($e->getMessage()); @@ -360,6 +405,12 @@ public static function begin( $errorOutput->writeLineFormatted('set reportUnmatchedIgnoredErrors: false in your configuration file.'); $errorOutput->writeLineFormatted(''); + 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')); @@ -368,9 +419,30 @@ public static function begin( $errorOutput->writeLineFormatted(''); } - $errorOutput->writeLineFormatted('If the excluded path can sometimes exist, append (?)'); - $errorOutput->writeLineFormatted('to its config entry to mark it as optional.'); - $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) { @@ -426,7 +498,10 @@ public static function begin( } if ($cleanupContainerCache) { - $containerFactory->clearOldContainers($tmpDir); + $cacheStorage = $container->getService('cacheStorage'); + if ($cacheStorage instanceof FileCacheStorage) { + $cacheStorage->clearUnusedFiles(); + } } /** @var bool|null $customRulesetUsed */ @@ -451,7 +526,7 @@ public static function begin( self::executeBootstrapFile($bootstrapFileFromArray, $container, $errorOutput, $debugEnabled); } - /** @var array|false $autoloadFunctionsAfter */ + /** @var list|false $autoloadFunctionsAfter */ $autoloadFunctionsAfter = spl_autoload_functions(); if ($autoloadFunctionsBefore !== false && $autoloadFunctionsAfter !== false) { @@ -516,74 +591,6 @@ public static function begin( throw new InceptionNotSuccessfulException(); } - $excludesAnalyse = $container->getParameter('excludes_analyse'); - $excludePaths = $container->getParameter('excludePaths'); - if (count($excludesAnalyse) > 0 && $excludePaths !== null) { - $errorOutput->writeLineFormatted(sprintf('Configuration parameters excludes_analyse and excludePaths cannot be used at the same time.')); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted(sprintf('Parameter excludes_analyse has been deprecated so use excludePaths only from now on.')); - $errorOutput->writeLineFormatted(''); - - throw new InceptionNotSuccessfulException(); - } elseif (count($excludesAnalyse) > 0) { - $errorOutput->writeLineFormatted('⚠️ You\'re using a deprecated config option excludes_analyse. ⚠️️'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted(sprintf('Parameter excludes_analyse has been deprecated so use excludePaths only from now on.')); - } - - if ($container->hasParameter('scopeClass') && $container->getParameter('scopeClass') !== MutatingScope::class) { - $errorOutput->writeLineFormatted('⚠️ You\'re using a deprecated config option scopeClass. ⚠️️'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted(sprintf('Please implement PHPStan\Type\ExpressionTypeResolverExtension interface instead and register it as a service.')); - } - - if ($projectConfig !== null) { - $parameters = $projectConfig['parameters'] ?? []; - /** @var bool $checkMissingIterableValueType */ - $checkMissingIterableValueType = $parameters['checkMissingIterableValueType'] ?? true; - if (!$checkMissingIterableValueType) { - $errorOutput->writeLineFormatted('⚠️ You\'re using a deprecated config option checkMissingIterableValueType ⚠️️'); - $errorOutput->writeLineFormatted(''); - - $featureToggles = $container->getParameter('featureToggles'); - if (!((bool) $featureToggles['bleedingEdge'])) { - $errorOutput->writeLineFormatted('It\'s strongly recommended to remove it from your configuration file'); - $errorOutput->writeLineFormatted('and add the missing array typehints.'); - $errorOutput->writeLineFormatted(''); - } - - $errorOutput->writeLineFormatted('If you want to continue ignoring missing typehints from arrays,'); - $errorOutput->writeLineFormatted('add missingType.iterableValue error identifier to your ignoreErrors:'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('parameters:'); - $errorOutput->writeLineFormatted("\tignoreErrors:"); - $errorOutput->writeLineFormatted("\t\t-"); - $errorOutput->writeLineFormatted("\t\t\tidentifier: missingType.iterableValue"); - $errorOutput->writeLineFormatted(''); - } - - /** @var bool $checkGenericClassInNonGenericObjectType */ - $checkGenericClassInNonGenericObjectType = $parameters['checkGenericClassInNonGenericObjectType'] ?? true; - if (!$checkGenericClassInNonGenericObjectType) { - $errorOutput->writeLineFormatted('⚠️ You\'re using a deprecated config option checkGenericClassInNonGenericObjectType ⚠️️'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('It\'s strongly recommended to remove it from your configuration file'); - $errorOutput->writeLineFormatted('and add the missing generic typehints.'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('If you want to continue ignoring missing typehints from generics,'); - $errorOutput->writeLineFormatted('add missingType.generics error identifier to your ignoreErrors:'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('parameters:'); - $errorOutput->writeLineFormatted("\tignoreErrors:"); - $errorOutput->writeLineFormatted("\t\t-"); - $errorOutput->writeLineFormatted("\t\t\tidentifier: missingType.generics"); - $errorOutput->writeLineFormatted(''); - } - } - - $tempResultCachePath = $container->getParameter('tempResultCachePath'); - $createDir($tempResultCachePath); - /** @var FileFinder $fileFinder */ $fileFinder = $container->getService('fileFinderAnalyse'); @@ -601,7 +608,7 @@ public static function begin( $pathRoutingParser->setAnalysedFiles($files); - $stubFilesExcluder = new FileExcluder($currentWorkingDirectoryFileHelper, $stubFilesProvider->getProjectStubFiles(), true); + $stubFilesExcluder = new FileExcluder($currentWorkingDirectoryFileHelper, $stubFilesProvider->getProjectStubFiles()); $files = array_values(array_filter($files, static fn (string $file) => !$stubFilesExcluder->isExcludedFromAnalysing($file))); @@ -617,6 +624,8 @@ public static function begin( $projectConfigFile, $projectConfig, $generateBaselineFile, + $singleReflectionFile, + $singleReflectionInsteadOfFile, ); } diff --git a/src/Command/DiagnoseCommand.php b/src/Command/DiagnoseCommand.php index 608cf732f1..03d8d78455 100644 --- a/src/Command/DiagnoseCommand.php +++ b/src/Command/DiagnoseCommand.php @@ -2,6 +2,7 @@ namespace PHPStan\Command; +use Override; use PHPStan\Diagnose\DiagnoseExtension; use PHPStan\Diagnose\PHPStanDiagnoseExtension; use PHPStan\ShouldNotHappenException; @@ -11,7 +12,7 @@ use Symfony\Component\Console\Output\OutputInterface; use function is_string; -class DiagnoseCommand extends Command +final class DiagnoseCommand extends Command { private const NAME = 'diagnose'; @@ -26,6 +27,7 @@ public function __construct( parent::__construct(); } + #[Override] protected function configure(): void { $this->setName(self::NAME) @@ -34,11 +36,12 @@ protected function configure(): void new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to project configuration file'), new InputOption(AnalyseCommand::OPTION_LEVEL, 'l', InputOption::VALUE_REQUIRED, 'Level of rule options - the higher the stricter'), new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), - new InputOption('debug', null, InputOption::VALUE_NONE, 'Show debug information - do not catch internal errors'), - new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for clearing result cache'), + 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')) { @@ -51,6 +54,7 @@ protected function initialize(InputInterface $input, OutputInterface $output): v } } + #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { $memoryLimit = $input->getOption('memory-limit'); @@ -79,6 +83,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int null, $level, false, + false, + null, + null, + false, ); } catch (InceptionNotSuccessfulException) { return 1; diff --git a/src/Command/DumpParametersCommand.php b/src/Command/DumpParametersCommand.php index 50e6e57b4b..4ea86d2125 100644 --- a/src/Command/DumpParametersCommand.php +++ b/src/Command/DumpParametersCommand.php @@ -4,6 +4,7 @@ use Nette\Neon\Neon; use Nette\Utils\Json; +use Override; use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -11,7 +12,7 @@ use Symfony\Component\Console\Output\OutputInterface; use function is_string; -class DumpParametersCommand extends Command +final class DumpParametersCommand extends Command { private const NAME = 'dump-parameters'; @@ -26,6 +27,7 @@ public function __construct( parent::__construct(); } + #[Override] protected function configure(): void { $this->setName(self::NAME) @@ -34,12 +36,13 @@ protected function configure(): void new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to project configuration file'), new InputOption(AnalyseCommand::OPTION_LEVEL, 'l', InputOption::VALUE_REQUIRED, 'Level of rule options - the higher the stricter'), new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), - new InputOption('debug', null, InputOption::VALUE_NONE, 'Show debug information - which file is analysed, do not catch internal errors'), - new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for clearing result cache'), - new InputOption('json', null, InputOption::VALUE_NONE, 'Dump parameters as JSON instead of NEON'), + 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')) { @@ -52,6 +55,7 @@ protected function initialize(InputInterface $input, OutputInterface $output): v } } + #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { $memoryLimit = $input->getOption('memory-limit'); @@ -81,6 +85,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int null, $level, false, + false, + null, + null, + false, ); } catch (InceptionNotSuccessfulException) { return 1; @@ -96,6 +104,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int unset($parameters['tempDir']); unset($parameters['__validate']); + // internal - editor mode + unset($parameters['singleReflectionFile']); + unset($parameters['singleReflectionInsteadOfFile']); + if ($json) { $encoded = Json::encode($parameters, Json::PRETTY); } else { diff --git a/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php b/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php index 545eeed111..ac02e1f9e1 100644 --- a/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php +++ b/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php @@ -9,12 +9,13 @@ use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; use PHPStan\ShouldNotHappenException; +use function count; use function ksort; use function preg_quote; use function substr; use const SORT_STRING; -class BaselineNeonErrorFormatter +final class BaselineNeonErrorFormatter { public function __construct(private RelativePathHelper $relativePathHelper) @@ -37,29 +38,57 @@ public function formatErrors( if (!$fileSpecificError->canBeIgnored()) { continue; } - $fileErrors[$this->relativePathHelper->getRelativePath($fileSpecificError->getFilePath())][] = $fileSpecificError->getMessage(); + $fileErrors[$this->relativePathHelper->getRelativePath($fileSpecificError->getFilePath())][] = $fileSpecificError; } ksort($fileErrors, SORT_STRING); $errorsToOutput = []; - foreach ($fileErrors as $file => $errorMessages) { - $fileErrorsCounts = []; - foreach ($errorMessages as $errorMessage) { - if (!isset($fileErrorsCounts[$errorMessage])) { - $fileErrorsCounts[$errorMessage] = 1; + foreach ($fileErrors as $file => $errors) { + $fileErrorsByMessage = []; + foreach ($errors as $error) { + $errorMessage = $error->getMessage(); + $identifier = $error->getIdentifier(); + if (!isset($fileErrorsByMessage[$errorMessage])) { + $fileErrorsByMessage[$errorMessage] = [ + 1, + $identifier !== null ? [$identifier => 1] : [], + ]; continue; } - $fileErrorsCounts[$errorMessage]++; + $fileErrorsByMessage[$errorMessage][0]++; + + if ($identifier === null) { + continue; + } + + if (!isset($fileErrorsByMessage[$errorMessage][1][$identifier])) { + $fileErrorsByMessage[$errorMessage][1][$identifier] = 1; + continue; + } + + $fileErrorsByMessage[$errorMessage][1][$identifier]++; } - ksort($fileErrorsCounts, SORT_STRING); - - foreach ($fileErrorsCounts as $message => $count) { - $errorsToOutput[] = [ - 'message' => Helpers::escape('#^' . preg_quote($message, '#') . '$#'), - 'count' => $count, - 'path' => Helpers::escape($file), - ]; + ksort($fileErrorsByMessage, SORT_STRING); + + foreach ($fileErrorsByMessage as $message => [$totalCount, $identifiers]) { + ksort($identifiers, SORT_STRING); + if (count($identifiers) > 0) { + foreach ($identifiers as $identifier => $identifierCount) { + $errorsToOutput[] = [ + 'message' => Helpers::escape('#^' . preg_quote($message, '#') . '$#'), + 'identifier' => $identifier, + 'count' => $identifierCount, + 'path' => Helpers::escape($file), + ]; + } + } else { + $errorsToOutput[] = [ + 'message' => Helpers::escape('#^' . preg_quote($message, '#') . '$#'), + 'count' => $totalCount, + 'path' => Helpers::escape($file), + ]; + } } } diff --git a/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php b/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php index 0af50dba47..65cafffb9f 100644 --- a/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php +++ b/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php @@ -6,17 +6,14 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; -use function array_keys; use function count; -use function implode; use function ksort; use function preg_quote; -use function sort; use function sprintf; use function var_export; use const SORT_STRING; -class BaselinePhpErrorFormatter +final class BaselinePhpErrorFormatter { public function __construct(private RelativePathHelper $relativePathHelper) @@ -54,42 +51,50 @@ public function formatErrors( $fileErrorsByMessage = []; foreach ($errors as $error) { $errorMessage = $error->getMessage(); + $identifier = $error->getIdentifier(); if (!isset($fileErrorsByMessage[$errorMessage])) { $fileErrorsByMessage[$errorMessage] = [ 1, - $error->getIdentifier() !== null ? [$error->getIdentifier() => true] : [], + $identifier !== null ? [$identifier => 1] : [], ]; continue; } $fileErrorsByMessage[$errorMessage][0]++; - if ($error->getIdentifier() === null) { + if ($identifier === null) { continue; } - $fileErrorsByMessage[$errorMessage][1][$error->getIdentifier()] = true; + + if (!isset($fileErrorsByMessage[$errorMessage][1][$identifier])) { + $fileErrorsByMessage[$errorMessage][1][$identifier] = 1; + continue; + } + + $fileErrorsByMessage[$errorMessage][1][$identifier]++; } ksort($fileErrorsByMessage, SORT_STRING); - foreach ($fileErrorsByMessage as $message => [$count, $identifiersInKeys]) { - $identifiers = array_keys($identifiersInKeys); - sort($identifiers); - $identifiersComment = ''; + foreach ($fileErrorsByMessage as $message => [$totalCount, $identifiers]) { + ksort($identifiers, SORT_STRING); if (count($identifiers) > 0) { - if (count($identifiers) === 1) { - $identifiersComment = "\n\t// identifier: " . $identifiers[0]; - } else { - $identifiersComment = "\n\t// identifiers: " . implode(', ', $identifiers); + foreach ($identifiers as $identifier => $identifierCount) { + $php .= sprintf( + "\$ignoreErrors[] = [\n\t'message' => %s,\n\t'identifier' => %s,\n\t'count' => %d,\n\t'path' => __DIR__ . %s,\n];\n", + var_export(Helpers::escape('#^' . preg_quote($message, '#') . '$#'), true), + var_export(Helpers::escape($identifier), true), + var_export($identifierCount, true), + var_export(Helpers::escape($file), true), + ); } + } else { + $php .= sprintf( + "\$ignoreErrors[] = [\n\t'message' => %s,\n\t'count' => %d,\n\t'path' => __DIR__ . %s,\n];\n", + var_export(Helpers::escape('#^' . preg_quote($message, '#') . '$#'), true), + var_export($totalCount, true), + var_export(Helpers::escape($file), true), + ); } - - $php .= sprintf( - "\$ignoreErrors[] = [%s\n\t'message' => %s,\n\t'count' => %d,\n\t'path' => __DIR__ . %s,\n];\n", - $identifiersComment, - var_export(Helpers::escape('#^' . preg_quote($message, '#') . '$#'), true), - var_export($count, true), - var_export(Helpers::escape($file), true), - ); } } diff --git a/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php b/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php index cfb2ddc559..8120dea1ef 100644 --- a/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php +++ b/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php @@ -5,6 +5,8 @@ use PHPStan\Analyser\Error; use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\File\RelativePathHelper; use function count; use function htmlspecialchars; @@ -12,10 +14,14 @@ use const ENT_COMPAT; use const ENT_XML1; -class CheckstyleErrorFormatter implements ErrorFormatter +#[AutowiredService(name: 'errorFormatter.checkstyle')] +final class CheckstyleErrorFormatter implements ErrorFormatter { - public function __construct(private RelativePathHelper $relativePathHelper) + public function __construct( + #[AutowiredParameter(ref: '@simpleRelativePathHelper')] + private RelativePathHelper $relativePathHelper, + ) { } diff --git a/src/Command/ErrorFormatter/CiDetectedErrorFormatter.php b/src/Command/ErrorFormatter/CiDetectedErrorFormatter.php index 541dfd77e3..403ebdb13f 100644 --- a/src/Command/ErrorFormatter/CiDetectedErrorFormatter.php +++ b/src/Command/ErrorFormatter/CiDetectedErrorFormatter.php @@ -6,9 +6,13 @@ use OndraM\CiDetector\Exception\CiNotDetectedException; use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; +use PHPStan\DependencyInjection\AutowiredService; -/** @api */ -class CiDetectedErrorFormatter implements ErrorFormatter +/** + * @api + */ +#[AutowiredService(as: CiDetectedErrorFormatter::class)] +final class CiDetectedErrorFormatter implements ErrorFormatter { public function __construct( diff --git a/src/Command/ErrorFormatter/GithubErrorFormatter.php b/src/Command/ErrorFormatter/GithubErrorFormatter.php index 03dc1aa8d0..5c383a06d8 100644 --- a/src/Command/ErrorFormatter/GithubErrorFormatter.php +++ b/src/Command/ErrorFormatter/GithubErrorFormatter.php @@ -4,6 +4,8 @@ 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; @@ -14,10 +16,12 @@ * 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 { public function __construct( + #[AutowiredParameter(ref: '@simpleRelativePathHelper')] private RelativePathHelper $relativePathHelper, ) { diff --git a/src/Command/ErrorFormatter/GitlabErrorFormatter.php b/src/Command/ErrorFormatter/GitlabErrorFormatter.php index 6aa4b61bef..a5c038320b 100644 --- a/src/Command/ErrorFormatter/GitlabErrorFormatter.php +++ b/src/Command/ErrorFormatter/GitlabErrorFormatter.php @@ -5,6 +5,8 @@ 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; @@ -12,10 +14,14 @@ /** * @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 { - public function __construct(private RelativePathHelper $relativePathHelper) + public function __construct( + #[AutowiredParameter(ref: '@simpleRelativePathHelper')] + private RelativePathHelper $relativePathHelper, + ) { } diff --git a/src/Command/ErrorFormatter/JsonErrorFormatter.php b/src/Command/ErrorFormatter/JsonErrorFormatter.php index 9f9f1edcfb..0a4174d4e0 100644 --- a/src/Command/ErrorFormatter/JsonErrorFormatter.php +++ b/src/Command/ErrorFormatter/JsonErrorFormatter.php @@ -5,11 +5,12 @@ use Nette\Utils\Json; use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; +use stdClass; use Symfony\Component\Console\Formatter\OutputFormatter; -use function array_key_exists; use function count; +use function property_exists; -class JsonErrorFormatter implements ErrorFormatter +final class JsonErrorFormatter implements ErrorFormatter { public function __construct(private bool $pretty) @@ -23,7 +24,7 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in 'errors' => count($analysisResult->getNotFileSpecificErrors()), 'file_errors' => count($analysisResult->getFileSpecificErrors()), ], - 'files' => [], + 'files' => new stdClass(), 'errors' => [], ]; @@ -31,13 +32,13 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in 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']++; $message = [ 'message' => $fileSpecificError->getMessage(), @@ -53,7 +54,7 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in $message['identifier'] = $fileSpecificError->getIdentifier(); } - $errorsArray['files'][$file]['messages'][] = $message; + $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 b7562f4733..b3d681ec62 100644 --- a/src/Command/ErrorFormatter/JunitErrorFormatter.php +++ b/src/Command/ErrorFormatter/JunitErrorFormatter.php @@ -4,16 +4,22 @@ 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 { - public function __construct(private RelativePathHelper $relativePathHelper) + public function __construct( + #[AutowiredParameter(ref: '@simpleRelativePathHelper')] + private RelativePathHelper $relativePathHelper, + ) { } diff --git a/src/Command/ErrorFormatter/RawErrorFormatter.php b/src/Command/ErrorFormatter/RawErrorFormatter.php index add31fa48f..a37dc42129 100644 --- a/src/Command/ErrorFormatter/RawErrorFormatter.php +++ b/src/Command/ErrorFormatter/RawErrorFormatter.php @@ -4,9 +4,11 @@ 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( diff --git a/src/Command/ErrorFormatter/TableErrorFormatter.php b/src/Command/ErrorFormatter/TableErrorFormatter.php index eb040735d0..b04b62964d 100644 --- a/src/Command/ErrorFormatter/TableErrorFormatter.php +++ b/src/Command/ErrorFormatter/TableErrorFormatter.php @@ -6,10 +6,11 @@ use PHPStan\Command\AnalyseCommand; use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\File\RelativePathHelper; use PHPStan\File\SimpleRelativePathHelper; use Symfony\Component\Console\Formatter\OutputFormatter; -use function array_key_exists; use function array_map; use function count; use function explode; @@ -17,19 +18,25 @@ use function in_array; use function is_string; use function ltrim; +use function rtrim; use function sprintf; use function str_contains; use function str_replace; -class TableErrorFormatter implements ErrorFormatter +#[AutowiredService(name: 'errorFormatter.table')] +final class TableErrorFormatter implements ErrorFormatter { public function __construct( 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, ) { @@ -69,36 +76,12 @@ public function formatErrors( /** @var array $fileErrors */ $fileErrors = []; - $outputIdentifiers = $output->isVerbose(); - $outputIdentifiersInFile = []; foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { if (!isset($fileErrors[$fileSpecificError->getFile()])) { $fileErrors[$fileSpecificError->getFile()] = []; } $fileErrors[$fileSpecificError->getFile()][] = $fileSpecificError; - if ($outputIdentifiers) { - continue; - } - - $filePath = $fileSpecificError->getTraitFilePath() ?? $fileSpecificError->getFilePath(); - if (array_key_exists($filePath, $outputIdentifiersInFile)) { - continue; - } - - if ($fileSpecificError->getIdentifier() === null) { - continue; - } - - if (!in_array($fileSpecificError->getIdentifier(), [ - 'ignore.unmatchedIdentifier', - 'ignore.parseError', - 'ignore.unmatched', - ], true)) { - continue; - } - - $outputIdentifiersInFile[$filePath] = true; } foreach ($fileErrors as $file => $errors) { @@ -106,7 +89,7 @@ public function formatErrors( foreach ($errors as $error) { $message = $error->getMessage(); $filePath = $error->getTraitFilePath() ?? $error->getFilePath(); - if (($outputIdentifiers || array_key_exists($filePath, $outputIdentifiersInFile)) && $error->getIdentifier() !== null && $error->canBeIgnored()) { + if ($error->getIdentifier() !== null && $error->canBeIgnored()) { $message .= "\n"; $message .= '🪪 ' . $error->getIdentifier(); } @@ -118,10 +101,11 @@ public function formatErrors( if (str_contains($tip, "\n")) { $lines = explode("\n", $tip); foreach ($lines as $line) { - $message .= '💡 ' . ltrim($line, ' •') . "\n"; + $message .= '💡 ' . ltrim($line, ' •') . "\n"; } + $message = rtrim($message, "\n"); } else { - $message .= '💡 ' . $tip; + $message .= '💡 ' . $tip; } } if (is_string($this->editorUrl)) { @@ -143,6 +127,14 @@ public function formatErrors( $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[] = [ $this->formatLineNumber($error->getLine()), $message, diff --git a/src/Command/ErrorFormatter/TeamcityErrorFormatter.php b/src/Command/ErrorFormatter/TeamcityErrorFormatter.php index a6e6e2cf32..3d00164719 100644 --- a/src/Command/ErrorFormatter/TeamcityErrorFormatter.php +++ b/src/Command/ErrorFormatter/TeamcityErrorFormatter.php @@ -4,21 +4,28 @@ 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 { - public function __construct(private RelativePathHelper $relativePathHelper) + public function __construct( + #[AutowiredParameter(ref: '@simpleRelativePathHelper')] + private RelativePathHelper $relativePathHelper, + ) { } @@ -41,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 520ce7add3..18301f5ab8 100644 --- a/src/Command/ErrorsConsoleStyle.php +++ b/src/Command/ErrorsConsoleStyle.php @@ -3,6 +3,8 @@ 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; @@ -13,12 +15,10 @@ use function explode; use function implode; use function sprintf; -use function str_starts_with; use function strlen; -use function wordwrap; use const DIRECTORY_SEPARATOR; -class ErrorsConsoleStyle extends SymfonyStyle +final class ErrorsConsoleStyle extends SymfonyStyle { public const OPTION_NO_PROGRESS = 'no-progress'; @@ -49,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 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; } @@ -63,11 +65,6 @@ public function table(array $headers, array $rows): void $maxHeaderWidth = $length; } - // manual wrapping could be replaced with $table->setColumnMaxWidth() - // but it's buggy for lines - // https://github.com/symfony/symfony/issues/45520 - // https://github.com/symfony/symfony/issues/45521 - $headers = $this->wrap($headers, $terminalWidth, $maxHeaderWidth); foreach ($headers as $i => $header) { $newHeader = []; foreach (explode("\n", $header) as $h) { @@ -77,11 +74,9 @@ public function table(array $headers, array $rows): void $headers[$i] = implode("\n", $newHeader); } - foreach ($rows as $i => $row) { - $rows[$i] = $this->wrap($row, $terminalWidth, $maxHeaderWidth); - } - $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); @@ -89,46 +84,7 @@ public function table(array $headers, array $rows): void $this->newLine(); } - /** - * @param string[] $rows - * @return string[] - */ - private function wrap(array $rows, int $terminalWidth, int $maxHeaderWidth): array - { - foreach ($rows as $i => $column) { - $columnRows = explode("\n", $column); - foreach ($columnRows as $k => $columnRow) { - if (str_starts_with($columnRow, '✏️')) { - continue; - } - $wrapped = wordwrap( - $columnRow, - $terminalWidth - $maxHeaderWidth - 5, - ); - if (str_starts_with($columnRow, '💡 ')) { - $wrappedLines = explode("\n", $wrapped); - $newWrappedLines = []; - foreach ($wrappedLines as $l => $line) { - if ($l === 0) { - $newWrappedLines[] = $line; - continue; - } - - $newWrappedLines[] = ' ' . $line; - } - $columnRows[$k] = implode("\n", $newWrappedLines); - } else { - $columnRows[$k] = $wrapped; - } - - } - - $rows[$i] = implode("\n", $columnRows); - } - - return $rows; - } - + #[Override] public function createProgressBar(int $max = 0): ProgressBar { $this->progressBar = parent::createProgressBar($max); @@ -180,6 +136,7 @@ private function getProgressBarFormat(): ?string return ProgressBar::getFormatDefinition($formatName); } + #[Override] public function progressStart(int $max = 0): void { if (!$this->showProgress) { @@ -188,6 +145,7 @@ public function progressStart(int $max = 0): void parent::progressStart($max); } + #[Override] public function progressAdvance(int $step = 1): void { if (!$this->showProgress) { @@ -197,6 +155,7 @@ public function progressAdvance(int $step = 1): void parent::progressAdvance($step); } + #[Override] public function progressFinish(): void { if (!$this->showProgress) { diff --git a/src/Command/FixerApplication.php b/src/Command/FixerApplication.php index 75caa6420c..0ee0689ede 100644 --- a/src/Command/FixerApplication.php +++ b/src/Command/FixerApplication.php @@ -12,6 +12,8 @@ use Phar; 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; @@ -49,6 +51,7 @@ 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; @@ -63,7 +66,8 @@ use const PHP_URL_PORT; use const PHP_VERSION_ID; -class FixerApplication +#[AutowiredService] +final class FixerApplication { /** @var PromiseInterface|null */ @@ -82,15 +86,25 @@ public function __construct( 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, ) { @@ -116,7 +130,7 @@ public function run( // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; // phpcs:enable - $decoder = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, 128 * 1024 * 1024); + $decoder = new Decoder($connection, true, options: $jsonInvalidUtf8Ignore, maxlength: 128 * 1024 * 1024); $encoder = new Encoder($connection, $jsonInvalidUtf8Ignore); $encoder->write(['action' => 'initialData', 'data' => [ 'currentWorkingDirectory' => $this->currentWorkingDirectory, @@ -146,7 +160,6 @@ public function run( }); $this->fileMonitor->initialize(array_merge( - $this->analysedPaths, $this->getComposerLocks(), $this->getComposerInstalled(), $this->getExecutedFiles(), @@ -240,10 +253,11 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc try { $phar = new Phar($pharPath); - } catch (Throwable) { + } catch (Throwable $e) { @unlink($pharPath); @unlink($infoPath); $output->writeln('PHPStan Pro PHAR signature is corrupted.'); + $output->writeln(sprintf('%s: %s', get_class($e), $e->getMessage())); throw new FixerProcessException(); } @@ -252,6 +266,7 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc @unlink($pharPath); @unlink($infoPath); $output->writeln('PHPStan Pro PHAR signature is corrupted.'); + $output->writeln(sprintf('Wrong hash type: %s', $phar->getSignature()['hash_type'])); throw new FixerProcessException(); } @@ -290,7 +305,7 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc } } - return new Process(sprintf('%s -d memory_limit=%s %s --port %d', escapeshellarg(PHP_BINARY), escapeshellarg(ini_get('memory_limit')), escapeshellarg($pharPath), $serverPort), null, $env, []); + 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( @@ -300,7 +315,7 @@ private function downloadPhar( ): void { $currentVersion = null; - $branch = '1.1.x'; + $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); @@ -323,6 +338,11 @@ private function downloadPhar( $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( new Connector( [ @@ -332,7 +352,9 @@ private function downloadPhar( ], 'dns' => $dnsConfig, ], + $loop, ), + $loop, ); /** @@ -373,7 +395,7 @@ private function downloadPhar( $this->printDownloadError($output, $e); }); - Loop::run(); + $loop->run(); fclose($pharPathResource); @@ -456,13 +478,13 @@ private function analyse( // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; // phpcs:enable - $decoder = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, 128 * 1024 * 1024); + $decoder = 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, 'changedFileAnalysis', ProcessHelper::getWorkerCommand( + $process = new ProcessPromise($loop, ProcessHelper::getWorkerCommand( $mainScript, 'fixer:worker', $projectConfigFile, diff --git a/src/Command/FixerProcessException.php b/src/Command/FixerProcessException.php index 9473f35716..c9e4097d58 100644 --- a/src/Command/FixerProcessException.php +++ b/src/Command/FixerProcessException.php @@ -4,7 +4,7 @@ use Exception; -class FixerProcessException extends Exception +final class FixerProcessException extends Exception { } diff --git a/src/Command/FixerWorkerCommand.php b/src/Command/FixerWorkerCommand.php index 3cc415b3f4..8701f1045d 100644 --- a/src/Command/FixerWorkerCommand.php +++ b/src/Command/FixerWorkerCommand.php @@ -3,6 +3,7 @@ namespace PHPStan\Command; use Clue\React\NDJson\Encoder; +use Override; use PHPStan\Analyser\AnalyserResult; use PHPStan\Analyser\AnalyserResultFinalizer; use PHPStan\Analyser\Error; @@ -42,7 +43,7 @@ use function usort; use const JSON_INVALID_UTF8_IGNORE; -class FixerWorkerCommand extends Command +final class FixerWorkerCommand extends Command { private const NAME = 'fixer:worker'; @@ -57,6 +58,7 @@ public function __construct( parent::__construct(); } + #[Override] protected function configure(): void { $this->setName(self::NAME) @@ -66,13 +68,14 @@ protected function configure(): void new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to project configuration file'), new InputOption(AnalyseCommand::OPTION_LEVEL, 'l', InputOption::VALUE_REQUIRED, 'Level of rule options - the higher the stricter'), new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), - new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for analysis'), - new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with Xdebug for debugging purposes'), - new InputOption('server-port', null, InputOption::VALUE_REQUIRED, 'Server port for FixerApplication'), + 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'); @@ -108,6 +111,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $level, $allowXdebug, false, + null, + null, false, ); } catch (InceptionNotSuccessfulException) { @@ -133,7 +138,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int //$in = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, 128 * 1024 * 1024); /** @var ResultCacheManager $resultCacheManager */ - $resultCacheManager = $container->getByType(ResultCacheManagerFactory::class)->create(); + $resultCacheManager = $container->getByType(ResultCacheManagerFactory::class)->create([]); $projectConfigArray = $inceptionResult->getProjectConfigArray(); /** @var AnalyserResultFinalizer $analyserResultFinalizer */ @@ -385,7 +390,7 @@ private function runAnalyser(LoopInterface $loop, Container $container, array $f $parallelAnalyser = $container->getByType(ParallelAnalyser::class); $filesCount = count($files); if ($filesCount === 0) { - return resolve(new AnalyserResult([], [], [], [], [], [], [], [], [], [], false, memory_get_peak_usage(true))); + return resolve(new AnalyserResult([], [], [], [], [], [], [], [], [], [], [], false, memory_get_peak_usage(true))); } /** @var Scheduler $scheduler */ @@ -406,6 +411,8 @@ private function runAnalyser(LoopInterface $loop, Container $container, array $f $mainScript, null, $configuration, + null, + null, $input, $onFileAnalysisHandler, ); diff --git a/src/Command/IgnoredRegexValidator.php b/src/Command/IgnoredRegexValidator.php index e7a90c5bd2..4340e0bd99 100644 --- a/src/Command/IgnoredRegexValidator.php +++ b/src/Command/IgnoredRegexValidator.php @@ -12,12 +12,10 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; use function count; -use function str_contains; -use function str_starts_with; use function strrpos; use function substr; -class IgnoredRegexValidator +final class IgnoredRegexValidator { public function __construct( @@ -34,19 +32,17 @@ public function validate(string $regex): IgnoredRegexValidatorResult try { /** @var TreeNode $ast */ $ast = $this->parser->parse($regex); - } catch (Exception $e) { - if (str_starts_with($e->getMessage(), 'Unexpected token "|" (alternation) at line 1')) { - return new IgnoredRegexValidatorResult([], false, true, '||', '\|\|'); - } - if ( - str_contains($regex, '()') - && str_starts_with($e->getMessage(), 'Unexpected token ")" (_capturing) at line 1') - ) { - return new IgnoredRegexValidatorResult([], false, true, '()', '\(\)'); - } + } catch (Exception) { return new IgnoredRegexValidatorResult([], false, false); } + if (Strings::match($regex, '~(?getIgnoredTypes($ast), $this->hasAnchorsInTheMiddle($ast), diff --git a/src/Command/IgnoredRegexValidatorResult.php b/src/Command/IgnoredRegexValidatorResult.php index fcda6a015b..0906a3c84b 100644 --- a/src/Command/IgnoredRegexValidatorResult.php +++ b/src/Command/IgnoredRegexValidatorResult.php @@ -2,7 +2,7 @@ namespace PHPStan\Command; -class IgnoredRegexValidatorResult +final class IgnoredRegexValidatorResult { /** diff --git a/src/Command/InceptionNotSuccessfulException.php b/src/Command/InceptionNotSuccessfulException.php index f17c3e08ec..5cbcf4425e 100644 --- a/src/Command/InceptionNotSuccessfulException.php +++ b/src/Command/InceptionNotSuccessfulException.php @@ -4,7 +4,7 @@ use Exception; -class InceptionNotSuccessfulException extends Exception +final class InceptionNotSuccessfulException extends Exception { } diff --git a/src/Command/InceptionResult.php b/src/Command/InceptionResult.php index 14a1d5202d..13ff361922 100644 --- a/src/Command/InceptionResult.php +++ b/src/Command/InceptionResult.php @@ -13,7 +13,7 @@ use function round; use function sprintf; -class InceptionResult +final class InceptionResult { /** @var callable(): (array{string[], bool}) */ @@ -32,6 +32,8 @@ public function __construct( private ?string $projectConfigFile, private ?array $projectConfigArray, private ?string $generateBaselineFile, + private ?string $editorModeTmpFile, + private ?string $editorModeInsteadOfFile, ) { $this->filesCallback = $filesCallback; @@ -88,12 +90,28 @@ public function getGenerateBaselineFile(): ?string return $this->generateBaselineFile; } + 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()) { + $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', - $this->formatDuration((int) round(microtime(true) - $analysisStartTime)), + $elapsedTimeString, )); } diff --git a/src/Command/Output.php b/src/Command/Output.php index 34b26c6c18..b0efcd648a 100644 --- a/src/Command/Output.php +++ b/src/Command/Output.php @@ -16,6 +16,10 @@ public function getStyle(): OutputStyle; public function isVerbose(): bool; + public function isVeryVerbose(): bool; + public function isDebug(): bool; + public function isDecorated(): bool; + } diff --git a/src/Command/Symfony/SymfonyOutput.php b/src/Command/Symfony/SymfonyOutput.php index 5e1a46273a..2d66f11a38 100644 --- a/src/Command/Symfony/SymfonyOutput.php +++ b/src/Command/Symfony/SymfonyOutput.php @@ -9,7 +9,7 @@ /** * @internal */ -class SymfonyOutput implements Output +final class SymfonyOutput implements Output { public function __construct( @@ -44,9 +44,19 @@ public function isVerbose(): bool return $this->symfonyOutput->isVerbose(); } + public function isVeryVerbose(): bool + { + return $this->symfonyOutput->isVeryVerbose(); + } + public function isDebug(): bool { return $this->symfonyOutput->isDebug(); } + public function isDecorated(): bool + { + return $this->symfonyOutput->isDecorated(); + } + } diff --git a/src/Command/Symfony/SymfonyStyle.php b/src/Command/Symfony/SymfonyStyle.php index 99ed87ec33..4008ec0b2a 100644 --- a/src/Command/Symfony/SymfonyStyle.php +++ b/src/Command/Symfony/SymfonyStyle.php @@ -8,18 +8,13 @@ /** * @internal */ -class SymfonyStyle implements OutputStyle +final class SymfonyStyle implements OutputStyle { public function __construct(private StyleInterface $symfonyStyle) { } - public function getSymfonyStyle(): StyleInterface - { - return $this->symfonyStyle; - } - public function title(string $message): void { $this->symfonyStyle->title($message); diff --git a/src/Command/WorkerCommand.php b/src/Command/WorkerCommand.php index 9646518839..dfef585109 100644 --- a/src/Command/WorkerCommand.php +++ b/src/Command/WorkerCommand.php @@ -4,6 +4,7 @@ use Clue\React\NDJson\Decoder; use Clue\React\NDJson\Encoder; +use Override; use PHPStan\Analyser\FileAnalyser; use PHPStan\Analyser\InternalError; use PHPStan\Analyser\NodeScopeResolver; @@ -24,7 +25,10 @@ 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; @@ -32,7 +36,7 @@ use function memory_get_peak_usage; use function sprintf; -class WorkerCommand extends Command +final class WorkerCommand extends Command { private const NAME = 'worker'; @@ -49,6 +53,7 @@ public function __construct( parent::__construct(); } + #[Override] protected function configure(): void { $this->setName(self::NAME) @@ -58,14 +63,17 @@ protected function configure(): void 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('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'); @@ -76,6 +84,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $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) @@ -86,6 +96,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int || (!is_bool($allowXdebug)) || !is_string($port) || !is_string($identifier) + || (!is_string($tmpFile) && $tmpFile !== null) + || (!is_string($insteadOfFile) && $insteadOfFile !== null) ) { throw new ShouldNotHappenException(); } @@ -103,6 +115,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $level, $allowXdebug, false, + $tmpFile, + $insteadOfFile, false, ); } catch (InceptionNotSuccessfulException $e) { @@ -114,6 +128,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int try { [$analysedFiles] = $inceptionResult->getFiles(); + $analysedFiles = $this->switchTmpFile($analysedFiles, $insteadOfFile, $tmpFile); } catch (PathNotFoundException $e) { $inceptionResult->getErrorOutput()->writeLineFormatted(sprintf('%s', $e->getMessage())); return 1; @@ -127,14 +142,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $analysedFiles = array_fill_keys($analysedFiles, true); $tcpConnector = new TcpConnector($loop); - $tcpConnector->connect(sprintf('127.0.0.1:%d', $port))->then(function (ConnectionInterface $connection) use ($container, $identifier, $output, $analysedFiles): void { + $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, 512, $jsonInvalidUtf8Ignore, $container->getParameter('parallel')['buffer']); + $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); + $this->runWorker($container, $out, $in, $output, $analysedFiles, $tmpFile, $insteadOfFile); }); $loop->run(); @@ -155,6 +170,8 @@ private function runWorker( ReadableStreamInterface $in, OutputInterface $output, array $analysedFiles, + ?string $tmpFile, + ?string $insteadOfFile, ): void { $handleError = function (Throwable $error) use ($out, $output): void { @@ -192,7 +209,7 @@ private function runWorker( $fileAnalyser = $container->getByType(FileAnalyser::class); $ruleRegistry = $container->getByType(RuleRegistry::class); $collectorRegistry = $container->getByType(CollectorRegistry::class); - $in->on('data', static function (array $json) use ($fileAnalyser, $ruleRegistry, $collectorRegistry, $out, $analysedFiles): void { + $in->on('data', static function (array $json) use ($fileAnalyser, $ruleRegistry, $collectorRegistry, $out, $analysedFiles, $tmpFile, $insteadOfFile): void { $action = $json['action']; if ($action !== 'analyse') { return; @@ -209,9 +226,13 @@ private function runWorker( $unmatchedLineIgnores = []; $collectedData = []; $dependencies = []; + $usedTraitDependencies = []; $exportedNodes = []; foreach ($files as $file) { try { + if ($file === $insteadOfFile) { + $file = $tmpFile; + } $fileAnalyserResult = $fileAnalyser->analyseFile($file, $analysedFiles, $ruleRegistry, $collectorRegistry, null); $fileErrors = $fileAnalyserResult->getErrors(); $filteredPhpErrors = array_merge($filteredPhpErrors, $fileAnalyserResult->getFilteredPhpErrors()); @@ -219,6 +240,7 @@ private function runWorker( $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; @@ -226,8 +248,12 @@ private function runWorker( foreach ($fileAnalyserResult->getLocallyIgnoredErrors() as $locallyIgnoredError) { $locallyIgnoredErrors[] = $locallyIgnoredError; } - foreach ($fileAnalyserResult->getCollectedData() as $data) { - $collectedData[] = $data; + foreach ($fileAnalyserResult->getCollectedData() as $collectedFile => $dataPerCollector) { + foreach ($dataPerCollector as $collectorType => $collectorData) { + foreach ($collectorData as $data) { + $collectedData[$collectedFile][$collectorType][] = $data; + } + } } } catch (Throwable $t) { $internalErrorsCount++; @@ -254,6 +280,7 @@ private function runWorker( 'collectedData' => $collectedData, 'memoryUsage' => memory_get_peak_usage(true), 'dependencies' => $dependencies, + 'usedTraitDependencies' => $usedTraitDependencies, 'exportedNodes' => $exportedNodes, 'files' => $files, 'internalErrorsCount' => $internalErrorsCount, @@ -262,4 +289,26 @@ private function runWorker( $in->on('error', $handleError); } + /** + * @param string[] $analysedFiles + * @return string[] + */ + private function switchTmpFile( + array $analysedFiles, + ?string $insteadOfFile, + ?string $tmpFile, + ): array + { + if ($insteadOfFile === null) { + return $analysedFiles; + } + $analysedFiles = array_values(array_filter($analysedFiles, static fn (string $file): bool => $file !== $insteadOfFile)); + + if ($tmpFile !== null) { + array_unshift($analysedFiles, $tmpFile); + } + + return $analysedFiles; + } + } diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index 0688b5d19e..c487355dcf 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -11,24 +11,26 @@ 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\Node\InPropertyHookNode; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\ClosureType; use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\Type; use function array_merge; use function count; -class DependencyResolver +#[AutowiredService] +final class DependencyResolver { public function __construct( @@ -45,7 +47,7 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies $dependenciesReflections = []; if ($node instanceof Node\Stmt\Class_) { - if ($node->namespacedName !== null) { + if (isset($node->namespacedName)) { $this->addClassToDependencies($node->namespacedName->toString(), $dependenciesReflections); } if ($node->extends !== null) { @@ -70,9 +72,8 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies } } elseif ($node instanceof InClassMethodNode) { $nativeMethod = $node->getMethodReflection(); - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($nativeMethod->getVariants()); $this->extractThrowType($nativeMethod->getThrowType(), $dependenciesReflections); - $this->extractFromParametersAcceptor($parametersAcceptor, $dependenciesReflections); + $this->extractFromParametersAcceptor($nativeMethod, $dependenciesReflections); foreach ($nativeMethod->getAsserts()->getAll() as $assertTag) { foreach ($assertTag->getType()->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); @@ -86,10 +87,13 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } + } elseif ($node instanceof InPropertyHookNode) { + $nativeMethod = $node->getHookReflection(); + $this->extractThrowType($nativeMethod->getThrowType(), $dependenciesReflections); + $this->extractFromParametersAcceptor($nativeMethod, $dependenciesReflections); } elseif ($node instanceof ClassPropertyNode) { - $nativeTypeNode = $node->getNativeType(); - if ($nativeTypeNode !== null) { - $nativeType = ParserNodeTypeToPHPStanType::resolve($nativeTypeNode, $node->getClassReflection()); + $nativeType = $node->getNativeType(); + if ($nativeType !== null) { foreach ($nativeType->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } @@ -103,9 +107,8 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies } elseif ($node instanceof InFunctionNode) { $functionReflection = $node->getFunctionReflection(); $this->extractThrowType($functionReflection->getThrowType(), $dependenciesReflections); - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()); - $this->extractFromParametersAcceptor($parametersAcceptor, $dependenciesReflections); + $this->extractFromParametersAcceptor($functionReflection, $dependenciesReflections); foreach ($functionReflection->getAsserts()->getAll() as $assertTag) { foreach ($assertTag->getType()->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); @@ -174,7 +177,7 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies } foreach ($variant->getParameters() as $parameter) { - if (!$parameter instanceof ParameterReflectionWithPhpDocs) { + if (!$parameter instanceof ExtendedParameterReflection) { continue; } if ($parameter->getOutType() !== null) { @@ -472,6 +475,16 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies return new NodeDependencies($this->fileHelper, $dependenciesReflections, $this->exportedNodeResolver->resolve($scope->getFile(), $node)); } + public function resolveUsedTraitDependencies(InClassNode $inClassNode): NodeDependencies + { + $dependenciesReflections = []; + foreach ($inClassNode->getClassReflection()->getTraits(true) as $trait) { + $dependenciesReflections[] = $trait; + } + + return new NodeDependencies($this->fileHelper, $dependenciesReflections, null); + } + private function considerArrayForCallableTest(Scope $scope, Array_ $arrayNode): bool { $items = $arrayNode->items; @@ -479,12 +492,8 @@ private function considerArrayForCallableTest(Scope $scope, Array_ $arrayNode): return false; } - if ($items[0] === null) { - return false; - } - $itemType = $scope->getType($items[0]->value); - return $itemType->isClassStringType()->yes(); + return $itemType->isClassString()->yes(); } /** @@ -505,7 +514,7 @@ private function addClassToDependencies(string $className, array &$dependenciesR $dependenciesReflections[] = $interface; } - foreach ($classReflection->getTraits() as $trait) { + foreach ($classReflection->getTraits(true) as $trait) { $dependenciesReflections[] = $trait; } @@ -527,6 +536,15 @@ private function addClassToDependencies(string $className, array &$dependenciesR } } + 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)) { @@ -534,6 +552,17 @@ private function addClassToDependencies(string $className, array &$dependenciesR } $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) { @@ -622,7 +651,7 @@ private function getFunctionReflection(Node\Name $nameNode, ?Scope $scope): Func * @param array $dependenciesReflections */ private function extractFromParametersAcceptor( - ParametersAcceptorWithPhpDocs $parametersAcceptor, + ExtendedParametersAcceptor $parametersAcceptor, array &$dependenciesReflections, ): void { diff --git a/src/Dependency/ExportedNode/ExportedAttributeNode.php b/src/Dependency/ExportedNode/ExportedAttributeNode.php index 02714f2f59..a3079eadce 100644 --- a/src/Dependency/ExportedNode/ExportedAttributeNode.php +++ b/src/Dependency/ExportedNode/ExportedAttributeNode.php @@ -3,11 +3,12 @@ namespace PHPStan\Dependency\ExportedNode; use JsonSerializable; +use Override; use PHPStan\Dependency\ExportedNode; use ReturnTypeWillChange; use function count; -class ExportedAttributeNode implements ExportedNode, JsonSerializable +final class ExportedAttributeNode implements ExportedNode, JsonSerializable { /** @@ -45,9 +46,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'], @@ -59,6 +59,7 @@ public static function __set_state(array $properties): ExportedNode * @return mixed */ #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ @@ -72,9 +73,8 @@ public function jsonSerialize() /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], diff --git a/src/Dependency/ExportedNode/ExportedClassConstantNode.php b/src/Dependency/ExportedNode/ExportedClassConstantNode.php index aab53925f1..abe08ceea8 100644 --- a/src/Dependency/ExportedNode/ExportedClassConstantNode.php +++ b/src/Dependency/ExportedNode/ExportedClassConstantNode.php @@ -3,13 +3,14 @@ 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 { /** @@ -45,9 +46,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'], @@ -58,9 +58,8 @@ public static function __set_state(array $properties): ExportedNode /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], @@ -78,6 +77,7 @@ public static function decode(array $data): ExportedNode * @return mixed */ #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ diff --git a/src/Dependency/ExportedNode/ExportedClassConstantsNode.php b/src/Dependency/ExportedNode/ExportedClassConstantsNode.php index 04bace7282..ed4fe82198 100644 --- a/src/Dependency/ExportedNode/ExportedClassConstantsNode.php +++ b/src/Dependency/ExportedNode/ExportedClassConstantsNode.php @@ -3,13 +3,14 @@ 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 ExportedClassConstantsNode implements ExportedNode, JsonSerializable +final class ExportedClassConstantsNode implements ExportedNode, JsonSerializable { /** @@ -54,9 +55,8 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['constants'], @@ -69,9 +69,8 @@ public static function __set_state(array $properties): ExportedNode /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( array_map(static function (array $constantData): ExportedClassConstantNode { @@ -91,6 +90,7 @@ public static function decode(array $data): ExportedNode * @return mixed */ #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ diff --git a/src/Dependency/ExportedNode/ExportedClassNode.php b/src/Dependency/ExportedNode/ExportedClassNode.php index b96096cff2..58c3d17fd4 100644 --- a/src/Dependency/ExportedNode/ExportedClassNode.php +++ b/src/Dependency/ExportedNode/ExportedClassNode.php @@ -3,6 +3,7 @@ namespace PHPStan\Dependency\ExportedNode; use JsonSerializable; +use Override; use PHPStan\Dependency\ExportedNode; use PHPStan\Dependency\RootExportedNode; use PHPStan\ShouldNotHappenException; @@ -10,7 +11,7 @@ use function array_map; use function count; -class ExportedClassNode implements RootExportedNode, JsonSerializable +final class ExportedClassNode implements RootExportedNode, JsonSerializable { /** @@ -96,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'], @@ -118,6 +118,7 @@ public static function __set_state(array $properties): ExportedNode * @return mixed */ #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ @@ -139,9 +140,8 @@ public function jsonSerialize() /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], @@ -171,6 +171,9 @@ public static function decode(array $data): ExportedNode ); } + /** + * @return self::TYPE_CLASS + */ public function getType(): string { return self::TYPE_CLASS; diff --git a/src/Dependency/ExportedNode/ExportedEnumCaseNode.php b/src/Dependency/ExportedNode/ExportedEnumCaseNode.php index da304b5893..eb77579c89 100644 --- a/src/Dependency/ExportedNode/ExportedEnumCaseNode.php +++ b/src/Dependency/ExportedNode/ExportedEnumCaseNode.php @@ -3,10 +3,11 @@ namespace PHPStan\Dependency\ExportedNode; use JsonSerializable; +use Override; use PHPStan\Dependency\ExportedNode; use ReturnTypeWillChange; -class ExportedEnumCaseNode implements ExportedNode, JsonSerializable +final class ExportedEnumCaseNode implements ExportedNode, JsonSerializable { public function __construct(private string $name, private ?string $value, private ?ExportedPhpDocNode $phpDoc) @@ -37,9 +38,8 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['name'], @@ -50,9 +50,8 @@ public static function __set_state(array $properties): ExportedNode /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], @@ -65,6 +64,7 @@ public static function decode(array $data): ExportedNode * @return mixed */ #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ diff --git a/src/Dependency/ExportedNode/ExportedEnumNode.php b/src/Dependency/ExportedNode/ExportedEnumNode.php index 3d75abd627..1378b5ea80 100644 --- a/src/Dependency/ExportedNode/ExportedEnumNode.php +++ b/src/Dependency/ExportedNode/ExportedEnumNode.php @@ -3,6 +3,7 @@ namespace PHPStan\Dependency\ExportedNode; use JsonSerializable; +use Override; use PHPStan\Dependency\ExportedNode; use PHPStan\Dependency\RootExportedNode; use PHPStan\ShouldNotHappenException; @@ -10,7 +11,7 @@ use function array_map; use function count; -class ExportedEnumNode implements RootExportedNode, JsonSerializable +final class ExportedEnumNode implements RootExportedNode, JsonSerializable { /** @@ -76,9 +77,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'], @@ -94,6 +94,7 @@ public static function __set_state(array $properties): ExportedNode * @return mixed */ #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ @@ -111,9 +112,8 @@ public function jsonSerialize() /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], @@ -134,6 +134,9 @@ public static function decode(array $data): ExportedNode ); } + /** + * @return self::TYPE_ENUM + */ public function getType(): string { return self::TYPE_ENUM; diff --git a/src/Dependency/ExportedNode/ExportedFunctionNode.php b/src/Dependency/ExportedNode/ExportedFunctionNode.php index fcc0f51ad5..5c618302a5 100644 --- a/src/Dependency/ExportedNode/ExportedFunctionNode.php +++ b/src/Dependency/ExportedNode/ExportedFunctionNode.php @@ -3,6 +3,7 @@ namespace PHPStan\Dependency\ExportedNode; use JsonSerializable; +use Override; use PHPStan\Dependency\ExportedNode; use PHPStan\Dependency\RootExportedNode; use PHPStan\ShouldNotHappenException; @@ -10,7 +11,7 @@ use function array_map; use function count; -class ExportedFunctionNode implements RootExportedNode, JsonSerializable +final class ExportedFunctionNode implements RootExportedNode, JsonSerializable { /** @@ -74,9 +75,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'], @@ -92,6 +92,7 @@ public static function __set_state(array $properties): ExportedNode * @return mixed */ #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ @@ -109,9 +110,8 @@ public function jsonSerialize() /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], @@ -133,6 +133,9 @@ public static function decode(array $data): ExportedNode ); } + /** + * @return self::TYPE_FUNCTION + */ public function getType(): string { return self::TYPE_FUNCTION; diff --git a/src/Dependency/ExportedNode/ExportedInterfaceNode.php b/src/Dependency/ExportedNode/ExportedInterfaceNode.php index 394eef707d..c6f9aad58a 100644 --- a/src/Dependency/ExportedNode/ExportedInterfaceNode.php +++ b/src/Dependency/ExportedNode/ExportedInterfaceNode.php @@ -3,13 +3,14 @@ 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 RootExportedNode, JsonSerializable +final class ExportedInterfaceNode implements RootExportedNode, JsonSerializable { /** @@ -56,9 +57,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'], @@ -72,6 +72,7 @@ public static function __set_state(array $properties): ExportedNode * @return mixed */ #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ @@ -87,9 +88,8 @@ public function jsonSerialize() /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], @@ -103,6 +103,9 @@ public static function decode(array $data): ExportedNode ); } + /** + * @return self::TYPE_INTERFACE + */ public function getType(): string { return self::TYPE_INTERFACE; diff --git a/src/Dependency/ExportedNode/ExportedMethodNode.php b/src/Dependency/ExportedNode/ExportedMethodNode.php index a60a6d205e..eaeda487c5 100644 --- a/src/Dependency/ExportedNode/ExportedMethodNode.php +++ b/src/Dependency/ExportedNode/ExportedMethodNode.php @@ -3,13 +3,14 @@ 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 { /** @@ -83,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'], @@ -106,6 +106,7 @@ public static function __set_state(array $properties): ExportedNode * @return mixed */ #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ @@ -128,9 +129,8 @@ public function jsonSerialize() /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], diff --git a/src/Dependency/ExportedNode/ExportedParameterNode.php b/src/Dependency/ExportedNode/ExportedParameterNode.php index d205afafdb..ab00eb5538 100644 --- a/src/Dependency/ExportedNode/ExportedParameterNode.php +++ b/src/Dependency/ExportedNode/ExportedParameterNode.php @@ -3,13 +3,14 @@ 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 { /** @@ -51,9 +52,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'], @@ -69,6 +69,7 @@ public static function __set_state(array $properties): ExportedNode * @return mixed */ #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ @@ -86,9 +87,8 @@ public function jsonSerialize() /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], diff --git a/src/Dependency/ExportedNode/ExportedPhpDocNode.php b/src/Dependency/ExportedNode/ExportedPhpDocNode.php index 64a37c96a4..0ce56bbb54 100644 --- a/src/Dependency/ExportedNode/ExportedPhpDocNode.php +++ b/src/Dependency/ExportedNode/ExportedPhpDocNode.php @@ -3,10 +3,11 @@ 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 { /** @@ -33,6 +34,7 @@ public function equals(ExportedNode $node): bool * @return mixed */ #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ @@ -48,18 +50,16 @@ public function jsonSerialize() /** * @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'], $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'], $data['constUses'] ?? []); } diff --git a/src/Dependency/ExportedNode/ExportedPropertiesNode.php b/src/Dependency/ExportedNode/ExportedPropertiesNode.php index c801f25642..80b6abc674 100644 --- a/src/Dependency/ExportedNode/ExportedPropertiesNode.php +++ b/src/Dependency/ExportedNode/ExportedPropertiesNode.php @@ -3,18 +3,20 @@ 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 ExportedPropertiesNode implements JsonSerializable, ExportedNode +final class ExportedPropertiesNode implements JsonSerializable, ExportedNode { /** * @param string[] $names * @param ExportedAttributeNode[] $attributes + * @param ExportedPropertyHookNode[] $hooks */ public function __construct( private array $names, @@ -24,7 +26,14 @@ public function __construct( private bool $private, private bool $static, private bool $readonly, + private bool $abstract, + private bool $final, + private bool $publicSet, + private bool $protectedSet, + private bool $privateSet, + private bool $virtual, private array $attributes, + private array $hooks, ) { } @@ -67,18 +76,33 @@ public function equals(ExportedNode $node): bool } } + 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->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 - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['names'], @@ -88,15 +112,21 @@ public static function __set_state(array $properties): ExportedNode $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 - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['names'], @@ -106,12 +136,24 @@ public static function decode(array $data): ExportedNode $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']), ); } @@ -119,6 +161,7 @@ public static function decode(array $data): ExportedNode * @return mixed */ #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ @@ -131,7 +174,14 @@ public function jsonSerialize() '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/ExportedTraitNode.php b/src/Dependency/ExportedNode/ExportedTraitNode.php index f90246ad15..1dfed9a9ca 100644 --- a/src/Dependency/ExportedNode/ExportedTraitNode.php +++ b/src/Dependency/ExportedNode/ExportedTraitNode.php @@ -3,54 +3,156 @@ 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 RootExportedNode, JsonSerializable +final class ExportedTraitNode implements RootExportedNode, JsonSerializable { - public function __construct(private 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, + ) { } 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; @@ -58,7 +160,7 @@ public function getType(): string public function getName(): string { - return $this->traitName; + return $this->name; } } diff --git a/src/Dependency/ExportedNode/ExportedTraitUseAdaptation.php b/src/Dependency/ExportedNode/ExportedTraitUseAdaptation.php index 57f3491010..3ddc6a18dd 100644 --- a/src/Dependency/ExportedNode/ExportedTraitUseAdaptation.php +++ b/src/Dependency/ExportedNode/ExportedTraitUseAdaptation.php @@ -3,10 +3,11 @@ 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 { /** @@ -59,9 +60,8 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['traitName'], @@ -74,9 +74,8 @@ public static function __set_state(array $properties): ExportedNode /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['traitName'], @@ -91,6 +90,7 @@ public static function decode(array $data): ExportedNode * @return mixed */ #[ReturnTypeWillChange] + #[Override] public function jsonSerialize() { return [ diff --git a/src/Dependency/ExportedNodeFetcher.php b/src/Dependency/ExportedNodeFetcher.php index 4d562f1275..e12bf67b9c 100644 --- a/src/Dependency/ExportedNodeFetcher.php +++ b/src/Dependency/ExportedNodeFetcher.php @@ -3,13 +3,17 @@ 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 { public function __construct( + #[AutowiredParameter(ref: '@defaultAnalysisParser')] private Parser $parser, private ExportedNodeVisitor $visitor, ) diff --git a/src/Dependency/ExportedNodeResolver.php b/src/Dependency/ExportedNodeResolver.php index 89d532512c..5a6afabe70 100644 --- a/src/Dependency/ExportedNodeResolver.php +++ b/src/Dependency/ExportedNodeResolver.php @@ -3,6 +3,7 @@ namespace PHPStan\Dependency; use PhpParser\Node; +use PhpParser\Node\Expr; use PhpParser\Node\Name; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; @@ -19,19 +20,28 @@ use PHPStan\Dependency\ExportedNode\ExportedParameterNode; use PHPStan\Dependency\ExportedNode\ExportedPhpDocNode; 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 implode; use function is_string; +use function sprintf; -class ExportedNodeResolver +#[AutowiredService] +final class ExportedNodeResolver { - public function __construct(private FileTypeMapper $fileTypeMapper, private ExprPrinter $exprPrinter) + public function __construct( + private ReflectionProvider $reflectionProvider, + private FileTypeMapper $fileTypeMapper, + private ExprPrinter $exprPrinter, + ) { } @@ -145,7 +155,52 @@ public function resolve(string $fileName, Node $node): ?RootExportedNode } if ($node instanceof Node\Stmt\Trait_ && isset($node->namespacedName)) { - return new ExportedTraitNode($node->namespacedName->toString()); + $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; + } + } + + $className = $node->namespacedName->toString(); + + return new ExportedTraitNode( + $className, + $this->exportPhpDocNode( + $fileName, + $className, + 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), + ); } if ($node instanceof Function_) { @@ -165,7 +220,7 @@ public function resolve(string $fileName, Node $node): ?RootExportedNode $docComment !== null ? $docComment->getText() : null, ), $node->byRef, - $this->printType($node->returnType), + NodeTypePrinter::printType($node->returnType), $this->exportParameterNodes($node->params), $this->exportAttributeNodes($node->attrGroups), ); @@ -174,48 +229,6 @@ public function resolve(string $fileName, Node $node): ?RootExportedNode return null; } - /** - * @param Node\Identifier|Node\Name|Node\ComplexType|null $type - */ - private function printType($type): ?string - { - if ($type === null) { - return null; - } - - if ($type instanceof Node\NullableType) { - return '?' . $this->printType($type->type); - } - - if ($type instanceof Node\UnionType) { - return implode('|', array_map(function ($innerType): string { - $printedType = $this->printType($innerType); - if ($printedType === null) { - throw new ShouldNotHappenException(); - } - - return $printedType; - }, $type->types)); - } - - if ($type instanceof Node\IntersectionType) { - return implode('&', array_map(function ($innerType): string { - $printedType = $this->printType($innerType); - if ($printedType === null) { - throw new ShouldNotHappenException(); - } - - return $printedType; - }, $type->types)); - } - - if ($type instanceof Node\Identifier || $type instanceof Name) { - return $type->toString(); - } - - throw new ShouldNotHappenException(); - } - /** * @param Node\Param[] $params * @return ExportedParameterNode[] @@ -243,7 +256,7 @@ 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, @@ -321,7 +334,7 @@ private function exportClassStatement(Node\Stmt $node, string $fileName, string $node->isAbstract(), $node->isFinal(), $node->isStatic(), - $this->printType($node->returnType), + NodeTypePrinter::printType($node->returnType), $this->exportParameterNodes($node->params), $this->exportAttributeNodes($node->attrGroups), ); @@ -335,20 +348,36 @@ private function exportClassStatement(Node\Stmt $node, string $fileName, string $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( - array_map(static fn (Node\Stmt\PropertyProperty $prop): string => $prop->name->toString(), $node->props), + $names, $this->exportPhpDocNode( $fileName, $namespacedName, null, $docComment !== null ? $docComment->getText() : null, ), - $this->printType($node->type), + NodeTypePrinter::printType($node->type), $node->isPublic(), $node->isPrivate(), $node->isStatic(), $node->isReadonly(), + $node->isAbstract(), + $node->isFinal(), + $node->isPublicSet(), + $node->isProtectedSet(), + $node->isPrivateSet(), + $virtual, $this->exportAttributeNodes($node->attrGroups), + $this->exportPropertyHooks($node->hooks, $fileName, $namespacedName), ); } @@ -424,4 +453,41 @@ private function exportAttributeNodes(array $attributeGroups): array 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 902ca005d6..7cd4610809 100644 --- a/src/Dependency/ExportedNodeVisitor.php +++ b/src/Dependency/ExportedNodeVisitor.php @@ -2,12 +2,13 @@ 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 ?string $fileName = null; @@ -37,6 +38,7 @@ public function getExportedNodes(): array return $this->currentNodes; } + #[Override] public function enterNode(Node $node): ?int { if ($this->fileName === null) { @@ -52,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 a31a6eeadd..ac30175b35 100644 --- a/src/Dependency/NodeDependencies.php +++ b/src/Dependency/NodeDependencies.php @@ -7,7 +7,7 @@ use PHPStan\Reflection\FunctionReflection; use function array_values; -class NodeDependencies +final class NodeDependencies { /** diff --git a/src/DependencyInjection/AutowiredAttributeServicesExtension.php b/src/DependencyInjection/AutowiredAttributeServicesExtension.php new file mode 100644 index 0000000000..c8dd87fa6c --- /dev/null +++ b/src/DependencyInjection/AutowiredAttributeServicesExtension.php @@ -0,0 +1,158 @@ + 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 e11f095fda..04272124a1 100644 --- a/src/DependencyInjection/ConditionalTagsExtension.php +++ b/src/DependencyInjection/ConditionalTagsExtension.php @@ -5,63 +5,29 @@ use Nette; use Nette\DI\CompilerExtension; use Nette\Schema\Expect; -use PHPStan\Analyser\TypeSpecifierFactory; -use PHPStan\Broker\BrokerFactory; -use PHPStan\Collectors\RegistryFactory as CollectorRegistryFactory; -use PHPStan\DependencyInjection\Type\LazyDynamicThrowTypeExtensionProvider; -use PHPStan\DependencyInjection\Type\LazyParameterClosureTypeExtensionProvider; -use PHPStan\DependencyInjection\Type\LazyParameterOutTypeExtensionProvider; -use PHPStan\Diagnose\DiagnoseExtension; -use PHPStan\Parser\RichParser; -use PHPStan\PhpDoc\StubFilesExtension; -use PHPStan\PhpDoc\TypeNodeResolverExtension; -use PHPStan\Rules\Constants\AlwaysUsedClassConstantsExtensionProvider; -use PHPStan\Rules\LazyRegistry; -use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; +use 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 CompilerExtension +final class ConditionalTagsExtension extends CompilerExtension { + #[Override] public function getConfigSchema(): Nette\Schema\Schema { - $bool = Expect::anyOf(Expect::bool(), Expect::listOf(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::EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG => $bool, - BrokerFactory::OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG => $bool, - BrokerFactory::ALLOWED_SUB_TYPES_CLASS_REFLECTION_EXTENSION_TAG => $bool, - LazyRegistry::RULE_TAG => $bool, - TypeNodeResolverExtension::EXTENSION_TAG => $bool, - StubFilesExtension::EXTENSION_TAG => $bool, - AlwaysUsedClassConstantsExtensionProvider::EXTENSION_TAG => $bool, - ReadWritePropertiesExtensionProvider::EXTENSION_TAG => $bool, - TypeSpecifierFactory::FUNCTION_TYPE_SPECIFYING_EXTENSION_TAG => $bool, - TypeSpecifierFactory::METHOD_TYPE_SPECIFYING_EXTENSION_TAG => $bool, - TypeSpecifierFactory::STATIC_METHOD_TYPE_SPECIFYING_EXTENSION_TAG => $bool, - RichParser::VISITOR_SERVICE_TAG => $bool, - CollectorRegistryFactory::COLLECTOR_TAG => $bool, - LazyDynamicThrowTypeExtensionProvider::FUNCTION_TAG => $bool, - LazyDynamicThrowTypeExtensionProvider::METHOD_TAG => $bool, - LazyDynamicThrowTypeExtensionProvider::STATIC_METHOD_TAG => $bool, - LazyParameterClosureTypeExtensionProvider::FUNCTION_TAG => $bool, - LazyParameterClosureTypeExtensionProvider::METHOD_TAG => $bool, - LazyParameterClosureTypeExtensionProvider::STATIC_METHOD_TAG => $bool, - LazyParameterOutTypeExtensionProvider::FUNCTION_TAG => $bool, - LazyParameterOutTypeExtensionProvider::METHOD_TAG => $bool, - LazyParameterOutTypeExtensionProvider::STATIC_METHOD_TAG => $bool, - DiagnoseExtension::EXTENSION_TAG => $bool, - ])->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 */ diff --git a/src/DependencyInjection/Configurator.php b/src/DependencyInjection/Configurator.php index 1c8b1e7bc6..1c918448ab 100644 --- a/src/DependencyInjection/Configurator.php +++ b/src/DependencyInjection/Configurator.php @@ -2,30 +2,48 @@ 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\Bootstrap\Configurator +final class Configurator extends \Nette\Bootstrap\Configurator { /** @var string[] */ private array $allConfigFiles = []; - public function __construct(private LoaderFactory $loaderFactory) + public function __construct(private LoaderFactory $loaderFactory, private bool $journalContainer) { parent::__construct(); } + #[Override] protected function createLoader(): Loader { return $this->loaderFactory->createLoader(); @@ -42,6 +60,7 @@ public function setAllConfigFiles(array $allConfigFiles): void /** * @return mixed[] */ + #[Override] protected function getDefaultParameters(): array { return []; @@ -52,6 +71,7 @@ public function getContainerCacheDirectory(): string return $this->getCacheDirectory() . '/nette.configurator'; } + #[Override] public function loadContainer(): string { $loader = new ContainerLoader( @@ -59,12 +79,118 @@ public function loadContainer(): string $this->staticParameters['debugMode'], ); - return $loader->load( + $attributesPhp = __DIR__ . '/../../vendor/attributes.php'; + + $className = $loader->load( [$this, 'generateContainer'], - [$this->staticParameters, array_keys($this->dynamicParameters), $this->configs, PHP_VERSION_ID - PHP_RELEASE_VERSION, NeonAdapter::CACHE_KEY, $this->getAllConfigFilesHashes()], + [ + $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 { diff --git a/src/DependencyInjection/Container.php b/src/DependencyInjection/Container.php index 07a7a574e1..005f72b3eb 100644 --- a/src/DependencyInjection/Container.php +++ b/src/DependencyInjection/Container.php @@ -10,14 +10,15 @@ public function hasService(string $serviceName): bool; /** * @return mixed + * @throws MissingServiceException */ public function getService(string $serviceName); /** - * @phpstan-template T of object - * @phpstan-param class-string $className - * @phpstan-return T - * @return mixed + * @template T of object + * @param class-string $className + * @return T + * @throws MissingServiceException */ public function getByType(string $className); diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index cb3bf3006a..38b6d24f54 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -22,7 +22,6 @@ use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; -use PHPStan\Broker\Broker; use PHPStan\Command\CommandHelper; use PHPStan\File\FileHelper; use PHPStan\Node\Printer\Printer; @@ -31,11 +30,9 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\Accessory\AccessoryArrayListType; -use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\ObjectType; -use Symfony\Component\Finder\Finder; use function array_diff_key; +use function array_key_exists; use function array_map; use function array_merge; use function array_unique; @@ -45,18 +42,17 @@ use function getenv; use function ini_get; use function is_array; -use function is_dir; use function is_file; use function is_readable; use function spl_object_id; use function sprintf; use function str_ends_with; use function substr; -use function time; -use function unlink; -/** @api */ -class ContainerFactory +/** + * @api + */ +final class ContainerFactory { private FileHelper $fileHelper; @@ -67,8 +63,10 @@ class ContainerFactory private static ?int $lastInitializedContainerId = null; + private bool $journalContainer = false; + /** @api */ - public function __construct(private string $currentWorkingDirectory, private bool $checkDuplicateFiles = false) + public function __construct(private string $currentWorkingDirectory) { $this->fileHelper = new FileHelper($currentWorkingDirectory); @@ -84,6 +82,11 @@ public function __construct(private string $currentWorkingDirectory, private boo $this->configDirectory = $originalRootDir . '/conf'; } + public function setJournalContainer(): void + { + $this->journalContainer = true; + } + /** * @param string[] $additionalConfigFiles * @param string[] $analysedPaths @@ -99,6 +102,8 @@ public function create( string $usedLevel = CommandHelper::DEFAULT_LEVEL, ?string $generateBaselineFile = null, ?string $cliAutoloadFile = null, + ?string $singleReflectionFile = null, + ?string $singleReflectionInsteadOfFile = null, ): Container { [$allConfigFiles, $projectConfig] = $this->detectDuplicateIncludedFiles( @@ -115,7 +120,8 @@ public function create( $this->rootDirectory, $this->currentWorkingDirectory, $generateBaselineFile, - )); + $projectConfig['expandRelativePaths'], + ), $this->journalContainer); $configurator->defaultExtensions = [ 'php' => PhpExtension::class, 'extensions' => ExtensionsExtension::class, @@ -133,11 +139,13 @@ public function create( 'generateBaselineFile' => $generateBaselineFile, 'usedLevel' => $usedLevel, 'cliAutoloadFile' => $cliAutoloadFile, + 'env' => getenv(), ]); $configurator->addDynamicParameters([ + 'singleReflectionFile' => $singleReflectionFile, + 'singleReflectionInsteadOfFile' => $singleReflectionInsteadOfFile, 'analysedPaths' => $analysedPaths, 'analysedPathsFromConfig' => $analysedPathsFromConfig, - 'env' => getenv(), ]); $configurator->addConfig($this->configDirectory . '/config.neon'); foreach ($additionalConfigFiles as $additionalConfigFile) { @@ -181,52 +189,12 @@ public static function postInitializeContainer(Container $container): void $container->getByType(Printer::class), ); - $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->getParameter('featureToggles')['bleedingEdge']); - AccessoryArrayListType::setListTypeEnabled($container->getParameter('featureToggles')['listType']); - TemplateTypeVariance::setInvarianceCompositionEnabled($container->getParameter('featureToggles')['invarianceComposition']); - } - - public function clearOldContainers(string $tempDirectory): void - { - $configurator = new Configurator(new LoaderFactory( - $this->fileHelper, - $this->rootDirectory, - $this->currentWorkingDirectory, - null, - )); - $configurator->setDebugMode(true); - $configurator->setTempDirectory($tempDirectory); - - $containerDirectory = $configurator->getContainerCacheDirectory(); - if (!is_dir($containerDirectory)) { - return; - } - - $finder = new Finder(); - $finder->name('Container_*')->in($containerDirectory); - $twoDaysAgo = time() - 24 * 60 * 60 * 2; - - foreach ($finder as $containerFile) { - $path = $containerFile->getRealPath(); - if ($path === false) { - continue; - } - if ($containerFile->getATime() > $twoDaysAgo) { - continue; - } - if ($containerFile->getCTime() > $twoDaysAgo) { - continue; - } - - @unlink($path); - } } public function getCurrentWorkingDirectory(): string @@ -255,7 +223,7 @@ private function detectDuplicateIncludedFiles( array $loaderParameters, ): array { - $neonAdapter = new NeonAdapter(); + $neonAdapter = new NeonAdapter([]); $phpAdapter = new PhpAdapter(); $allConfigFiles = []; $configArray = []; @@ -274,10 +242,6 @@ private function detectDuplicateIncludedFiles( return [$normalized, $configArray]; } - if (!$this->checkDuplicateFiles) { - return [$normalized, $configArray]; - } - $duplicateFiles = array_unique(array_diff_key($normalized, $deduplicated)); throw new DuplicateIncludedFilesException($duplicateFiles); @@ -352,6 +316,18 @@ private function validateParameters(array $parameters, array $parametersSchema): $context->path = ['parameters']; }; $processor->process($schema, $parameters); + + if ( + !array_key_exists('phpVersion', $parameters) + || !is_array($parameters['phpVersion'])) { + return; + } + + $phpVersion = $parameters['phpVersion']; + + if ($phpVersion['max'] < $phpVersion['min']) { + throw new InvalidPhpVersionException('Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.'); + } } /** diff --git a/src/DependencyInjection/DerivativeContainerFactory.php b/src/DependencyInjection/DerivativeContainerFactory.php index 48270f714d..ffafdd548e 100644 --- a/src/DependencyInjection/DerivativeContainerFactory.php +++ b/src/DependencyInjection/DerivativeContainerFactory.php @@ -4,7 +4,8 @@ use function array_merge; -class DerivativeContainerFactory +#[AutowiredService] +final class DerivativeContainerFactory { /** @@ -14,15 +15,28 @@ class DerivativeContainerFactory * @param string[] $analysedPathsFromConfig */ public function __construct( + #[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, ) { } @@ -35,6 +49,7 @@ public function create(array $additionalConfigFiles): Container $containerFactory = new ContainerFactory( $this->currentWorkingDirectory, ); + $containerFactory->setJournalContainer(); return $containerFactory->create( $this->tempDirectory, @@ -45,6 +60,8 @@ public function create(array $additionalConfigFiles): Container $this->usedLevel, $this->generateBaselineFile, $this->cliAutoloadFile, + $this->singleReflectionFile, + $this->singleReflectionInsteadOfFile, ); } diff --git a/src/DependencyInjection/DuplicateIncludedFilesException.php b/src/DependencyInjection/DuplicateIncludedFilesException.php index 13ea681636..e377e42553 100644 --- a/src/DependencyInjection/DuplicateIncludedFilesException.php +++ b/src/DependencyInjection/DuplicateIncludedFilesException.php @@ -6,7 +6,7 @@ use function implode; use function sprintf; -class DuplicateIncludedFilesException extends Exception +final class DuplicateIncludedFilesException extends Exception { /** 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) + public function __construct(private array $errors, private array $suggestOptional) { parent::__construct(implode("\n", $this->errors)); } @@ -24,4 +25,12 @@ public function getErrors(): array return $this->errors; } + /** + * @return array{analyse?: list, analyseAndScan?: list} + */ + public function getSuggestOptional(): array + { + return $this->suggestOptional; + } + } diff --git a/src/DependencyInjection/InvalidIgnoredErrorPatternsException.php b/src/DependencyInjection/InvalidIgnoredErrorPatternsException.php index dd9258c64d..bd4f2466a1 100644 --- a/src/DependencyInjection/InvalidIgnoredErrorPatternsException.php +++ b/src/DependencyInjection/InvalidIgnoredErrorPatternsException.php @@ -5,7 +5,7 @@ use Exception; use function implode; -class InvalidIgnoredErrorPatternsException extends Exception +final class InvalidIgnoredErrorPatternsException extends Exception { /** diff --git a/src/DependencyInjection/InvalidPhpVersionException.php b/src/DependencyInjection/InvalidPhpVersionException.php new file mode 100644 index 0000000000..f9b41690a3 --- /dev/null +++ b/src/DependencyInjection/InvalidPhpVersionException.php @@ -0,0 +1,10 @@ + $expandRelativePaths + */ public function __construct( private FileHelper $fileHelper, private string $rootDir, private string $currentWorkingDirectory, private ?string $generateBaselineFile, + private array $expandRelativePaths, ) { } 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, diff --git a/src/DependencyInjection/MemoizingContainer.php b/src/DependencyInjection/MemoizingContainer.php index 07e92fbac5..dae8456aba 100644 --- a/src/DependencyInjection/MemoizingContainer.php +++ b/src/DependencyInjection/MemoizingContainer.php @@ -4,13 +4,17 @@ use function array_key_exists; -class MemoizingContainer implements Container +#[AutowiredService(as: Container::class)] +final class MemoizingContainer implements Container { /** @var array */ private array $servicesByType = []; - public function __construct(private Container $originalContainer) + public function __construct( + #[AutowiredParameter(ref: '@PHPStan\DependencyInjection\Nette\NetteContainer')] + private Container $originalContainer, + ) { } 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); @@ -119,28 +126,7 @@ public function process(array $arr, string $fileKey, string $file): array } } - 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][pro][tmpDir]', - '[parameters][memoryLimitFile]', - '[parameters][benchmarkFile]', - '[parameters][stubFiles][]', - '[parameters][symfony][console_application_loader]', - '[parameters][symfony][consoleApplicationLoader]', - '[parameters][symfony][container_xml_path]', - '[parameters][symfony][containerXmlPath]', - '[parameters][doctrine][objectManagerLoader]', - ], true) && is_string($val) && !str_contains($val, '%') && !str_starts_with($val, '*')) { + 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)); } @@ -158,58 +144,6 @@ public function process(array $arr, string $fileKey, string $file): array return $res; } - /** - * @param mixed[] $data - */ - 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 4f797d1af7..6664157fcf 100644 --- a/src/DependencyInjection/NeonLoader.php +++ b/src/DependencyInjection/NeonLoader.php @@ -3,9 +3,10 @@ namespace PHPStan\DependencyInjection; use Nette\DI\Config\Loader; +use Override; use PHPStan\File\FileHelper; -class NeonLoader extends Loader +final class NeonLoader extends Loader { public function __construct( @@ -18,6 +19,7 @@ public function __construct( /** * @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 b5e488ca32..842c76d159 100644 --- a/src/DependencyInjection/Nette/NetteContainer.php +++ b/src/DependencyInjection/Nette/NetteContainer.php @@ -2,7 +2,9 @@ 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; @@ -11,7 +13,8 @@ /** * @internal */ -class NetteContainer implements Container +#[AutowiredService(as: NetteContainer::class)] +final class NetteContainer implements Container { public function __construct(private \Nette\DI\Container $container) @@ -28,18 +31,25 @@ public function hasService(string $serviceName): bool */ 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); + } } /** - * @phpstan-template T of object - * @phpstan-param class-string $className - * @phpstan-return T - * @return mixed + * @template T of object + * @param class-string $className + * @return T */ public function getByType(string $className) { - return $this->container->getByType($className); + try { + return $this->container->getByType($className); + } catch (\Nette\DI\MissingServiceException $e) { + throw new MissingServiceException($e->getMessage(), previous: $e); + } } /** 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); diff --git a/src/DependencyInjection/ProjectConfigHelper.php b/src/DependencyInjection/ProjectConfigHelper.php index 0ac6ef97a2..47e8481399 100644 --- a/src/DependencyInjection/ProjectConfigHelper.php +++ b/src/DependencyInjection/ProjectConfigHelper.php @@ -9,7 +9,7 @@ use function is_array; use function is_string; -class ProjectConfigHelper +final class ProjectConfigHelper { /** diff --git a/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php b/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php index 899b9153d4..a292eba2ab 100644 --- a/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php +++ b/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php @@ -2,18 +2,23 @@ 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; +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; -class LazyClassReflectionExtensionRegistryProvider implements ClassReflectionExtensionRegistryProvider +#[AutowiredService(as: ClassReflectionExtensionRegistryProvider::class)] +final class LazyClassReflectionExtensionRegistryProvider implements ClassReflectionExtensionRegistryProvider { private ?ClassReflectionExtensionRegistry $registry = null; @@ -29,10 +34,14 @@ public function getRegistry(): ClassReflectionExtensionRegistry $annotationsMethodsClassReflectionExtension = $this->container->getByType(AnnotationsMethodsClassReflectionExtension::class); $annotationsPropertiesClassReflectionExtension = $this->container->getByType(AnnotationsPropertiesClassReflectionExtension::class); + $mixinMethodsClassReflectionExtension = $this->container->getByType(MixinMethodsClassReflectionExtension::class); + $mixinPropertiesClassReflectionExtension = $this->container->getByType(MixinPropertiesClassReflectionExtension::class); + $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([$phpClassReflectionExtension], $this->container->getServicesByTag(BrokerFactory::PROPERTIES_CLASS_REFLECTION_EXTENSION_TAG), [$annotationsPropertiesClassReflectionExtension, $mixinPropertiesClassReflectionExtension, $universalObjectCratesClassReflectionExtension]), + array_merge([$phpClassReflectionExtension], $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), 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 @@ +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 4696fb8b21..1a56f43268 100644 --- a/src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php +++ b/src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php @@ -2,9 +2,11 @@ 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'; diff --git a/src/DependencyInjection/Type/LazyExpressionTypeResolverExtensionRegistryProvider.php b/src/DependencyInjection/Type/LazyExpressionTypeResolverExtensionRegistryProvider.php index 43910f8668..6efc5fcb80 100644 --- a/src/DependencyInjection/Type/LazyExpressionTypeResolverExtensionRegistryProvider.php +++ b/src/DependencyInjection/Type/LazyExpressionTypeResolverExtensionRegistryProvider.php @@ -3,10 +3,12 @@ namespace PHPStan\DependencyInjection\Type; use PHPStan\Broker\BrokerFactory; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; use PHPStan\Type\ExpressionTypeResolverExtensionRegistry; -class LazyExpressionTypeResolverExtensionRegistryProvider implements ExpressionTypeResolverExtensionRegistryProvider +#[AutowiredService(as: ExpressionTypeResolverExtensionRegistryProvider::class)] +final class LazyExpressionTypeResolverExtensionRegistryProvider implements ExpressionTypeResolverExtensionRegistryProvider { private ?ExpressionTypeResolverExtensionRegistry $registry = null; @@ -17,13 +19,9 @@ public function __construct(private Container $container) public function getRegistry(): ExpressionTypeResolverExtensionRegistry { - if ($this->registry === null) { - $this->registry = new ExpressionTypeResolverExtensionRegistry( - $this->container->getServicesByTag(BrokerFactory::EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG), - ); - } - - return $this->registry; + return $this->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 22e356fe70..ead76923a3 100644 --- a/src/DependencyInjection/Type/LazyOperatorTypeSpecifyingExtensionRegistryProvider.php +++ b/src/DependencyInjection/Type/LazyOperatorTypeSpecifyingExtensionRegistryProvider.php @@ -2,12 +2,13 @@ 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 ?OperatorTypeSpecifyingExtensionRegistry $registry = null; @@ -18,14 +19,9 @@ public function __construct(private 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/LazyParameterClosureTypeExtensionProvider.php b/src/DependencyInjection/Type/LazyParameterClosureTypeExtensionProvider.php index 9b1db71fc5..ecc30869f5 100644 --- a/src/DependencyInjection/Type/LazyParameterClosureTypeExtensionProvider.php +++ b/src/DependencyInjection/Type/LazyParameterClosureTypeExtensionProvider.php @@ -2,9 +2,11 @@ namespace PHPStan\DependencyInjection\Type; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; -class LazyParameterClosureTypeExtensionProvider implements ParameterClosureTypeExtensionProvider +#[AutowiredService(as: ParameterClosureTypeExtensionProvider::class)] +final class LazyParameterClosureTypeExtensionProvider implements ParameterClosureTypeExtensionProvider { public const FUNCTION_TAG = 'phpstan.functionParameterClosureTypeExtension'; diff --git a/src/DependencyInjection/Type/LazyParameterOutTypeExtensionProvider.php b/src/DependencyInjection/Type/LazyParameterOutTypeExtensionProvider.php index 3cf13bf0ba..113eea7b29 100644 --- a/src/DependencyInjection/Type/LazyParameterOutTypeExtensionProvider.php +++ b/src/DependencyInjection/Type/LazyParameterOutTypeExtensionProvider.php @@ -2,9 +2,11 @@ namespace PHPStan\DependencyInjection\Type; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; -class LazyParameterOutTypeExtensionProvider implements ParameterOutTypeExtensionProvider +#[AutowiredService(as: ParameterOutTypeExtensionProvider::class)] +final class LazyParameterOutTypeExtensionProvider implements ParameterOutTypeExtensionProvider { public const FUNCTION_TAG = 'phpstan.functionParameterOutTypeExtension'; diff --git a/src/DependencyInjection/ValidateExcludePathsExtension.php b/src/DependencyInjection/ValidateExcludePathsExtension.php index 3d6d66763c..ed06c4c3b8 100644 --- a/src/DependencyInjection/ValidateExcludePathsExtension.php +++ b/src/DependencyInjection/ValidateExcludePathsExtension.php @@ -3,22 +3,23 @@ namespace PHPStan\DependencyInjection; use Nette\DI\CompilerExtension; +use Override; use PHPStan\DependencyInjection\Neon\OptionalPath; use PHPStan\File\FileExcluder; use function array_key_exists; use function array_map; -use function array_merge; use function count; use function is_dir; use function is_file; use function sprintf; -class ValidateExcludePathsExtension extends CompilerExtension +final class ValidateExcludePathsExtension extends CompilerExtension { /** * @throws InvalidExcludePathsException */ + #[Override] public function loadConfiguration(): void { $builder = $this->getContainerBuilder(); @@ -27,42 +28,42 @@ public function loadConfiguration(): void return; } + $newExcludePaths = []; + if (array_key_exists('analyseAndScan', $excludePaths)) { + $newExcludePaths['analyseAndScan'] = $excludePaths['analyseAndScan']; + } + if (array_key_exists('analyse', $excludePaths)) { + $newExcludePaths['analyse'] = $excludePaths['analyse']; + } + $errors = []; - $noImplicitWildcard = $builder->parameters['featureToggles']['noImplicitWildcard']; - if ($builder->parameters['__validate'] && $noImplicitWildcard) { - $paths = []; - if (array_key_exists('analyse', $excludePaths)) { - $paths = $excludePaths['analyse']; - } - if (array_key_exists('analyseAndScan', $excludePaths)) { - $paths = array_merge($paths, $excludePaths['analyseAndScan']); - } - foreach ($paths as $path) { - if ($path instanceof OptionalPath) { - continue; - } - if (FileExcluder::isAbsolutePath($path)) { - if (is_dir($path)) { + $suggestOptional = []; + if ($builder->parameters['__validate']) { + foreach ($newExcludePaths as $key => $paths) { + foreach ($paths as $path) { + if ($path instanceof OptionalPath) { continue; } - if (is_file($path)) { + if (FileExcluder::isAbsolutePath($path)) { + if (is_dir($path)) { + continue; + } + if (is_file($path)) { + continue; + } + } + if (FileExcluder::isFnmatchPattern($path)) { continue; } - } - if (FileExcluder::isFnmatchPattern($path)) { - continue; - } - $errors[] = sprintf('Path %s is neither a directory, nor a file path, nor a fnmatch pattern.', $path); + $suggestOptional[$key][] = $path; + $errors[] = sprintf('Path "%s" is neither a directory, nor a file path, nor a fnmatch pattern.', $path); + } } } - $newExcludePaths = []; - if (array_key_exists('analyseAndScan', $excludePaths)) { - $newExcludePaths['analyseAndScan'] = $excludePaths['analyseAndScan']; - } - if (array_key_exists('analyse', $excludePaths)) { - $newExcludePaths['analyse'] = $excludePaths['analyse']; + if (count($errors) !== 0) { + throw new InvalidExcludePathsException($errors, $suggestOptional); } foreach ($newExcludePaths as $key => $p) { @@ -73,12 +74,6 @@ public function loadConfiguration(): void } $builder->parameters['excludePaths'] = $newExcludePaths; - - if (count($errors) === 0) { - return; - } - - throw new InvalidExcludePathsException($errors); } } diff --git a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php index fea4461b45..afa8ee0ad1 100644 --- a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php +++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php @@ -7,11 +7,13 @@ use Nette\DI\CompilerExtension; use Nette\Utils\RegexpException; use Nette\Utils\Strings; +use Override; use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\NameScope; use PHPStan\Command\IgnoredRegexValidator; use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; use PHPStan\File\FileExcluder; +use PHPStan\Php\ComposerPhpVersionFactory; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\DirectTypeNodeResolverExtensionRegistryProvider; use PHPStan\PhpDoc\TypeNodeResolver; @@ -20,6 +22,7 @@ use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\TypeParser; +use PHPStan\PhpDocParser\ParserConfig; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider; @@ -40,12 +43,13 @@ use function sprintf; use const PHP_VERSION_ID; -class ValidateIgnoredErrorsExtension extends CompilerExtension +final class ValidateIgnoredErrorsExtension extends CompilerExtension { /** * @throws InvalidIgnoredErrorPatternsException */ + #[Override] public function loadConfiguration(): void { $builder = $this->getContainerBuilder(); @@ -58,20 +62,21 @@ public function loadConfiguration(): void return; } - $noImplicitWildcard = $builder->parameters['featureToggles']['noImplicitWildcard']; - /** @throws void */ $parser = Llk::load(new Read(__DIR__ . '/../../resources/RegexGrammar.pp')); $reflectionProvider = new DummyReflectionProvider(); $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider); ReflectionProviderStaticAccessor::registerInstance($reflectionProvider); PhpVersionStaticAccessor::registerInstance(new PhpVersion(PHP_VERSION_ID)); - $constantResolver = new ConstantResolver($reflectionProviderProvider, []); + $composerPhpVersionFactory = new ComposerPhpVersionFactory([]); + $constantResolver = new ConstantResolver($reflectionProviderProvider, [], null, $composerPhpVersionFactory); + + $phpDocParserConfig = new ParserConfig([]); $ignoredRegexValidator = new IgnoredRegexValidator( $parser, new TypeStringResolver( - new Lexer(), - new TypeParser(new ConstExprParser($builder->parameters['featureToggles']['unescapeStrings'])), + new Lexer($phpDocParserConfig), + new TypeParser($phpDocParserConfig, new ConstExprParser($phpDocParserConfig)), new TypeNodeResolver( new DirectTypeNodeResolverExtensionRegistryProvider( new class implements TypeNodeResolverExtensionRegistry { @@ -102,10 +107,10 @@ public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry { - return new OperatorTypeSpecifyingExtensionRegistry(null, []); + return new OperatorTypeSpecifyingExtensionRegistry([]); } - }, new OversizedArrayBuilder()), + }, new OversizedArrayBuilder(), true), ), ), ); @@ -138,7 +143,7 @@ public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry $reportUnmatched = (bool) $builder->parameters['reportUnmatchedIgnoredErrors']; - if ($noImplicitWildcard && $reportUnmatched) { + if ($reportUnmatched) { foreach ($ignoreErrors as $ignoreError) { if (!is_array($ignoreError)) { continue; @@ -165,7 +170,7 @@ public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry continue; } - $errors[] = sprintf('Path %s is neither a directory, nor a file path, nor a fnmatch pattern.', $ignorePath); + $errors[] = sprintf('Path "%s" is neither a directory, nor a file path, nor a fnmatch pattern.', $ignorePath); } } } diff --git a/src/DependencyInjection/ValidateServiceTagsExtension.php b/src/DependencyInjection/ValidateServiceTagsExtension.php new file mode 100644 index 0000000000..47f359f425 --- /dev/null +++ b/src/DependencyInjection/ValidateServiceTagsExtension.php @@ -0,0 +1,147 @@ + 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, + 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 ($definition->getTags() as $tag => $attr) { + 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/PHPStanDiagnoseExtension.php b/src/Diagnose/PHPStanDiagnoseExtension.php index b5ace25727..35cb6a862e 100644 --- a/src/Diagnose/PHPStanDiagnoseExtension.php +++ b/src/Diagnose/PHPStanDiagnoseExtension.php @@ -7,6 +7,7 @@ use PHPStan\ExtensionInstaller\GeneratedConfig; use PHPStan\File\FileHelper; use PHPStan\Internal\ComposerHelper; +use PHPStan\Php\ComposerPhpVersionFactory; use PHPStan\Php\PhpVersion; use ReflectionClass; use function array_key_exists; @@ -17,6 +18,7 @@ use function explode; use function implode; use function in_array; +use function is_array; use function is_file; use function is_readable; use function sprintf; @@ -25,18 +27,21 @@ use function substr; use const PHP_VERSION_ID; -class PHPStanDiagnoseExtension implements DiagnoseExtension +final class PHPStanDiagnoseExtension implements DiagnoseExtension { /** + * @param int|array{min: int, max: int}|null $configPhpVersion * @param string[] $composerAutoloaderProjectPaths * @param string [] $allConfigFiles */ public function __construct( private PhpVersion $phpVersion, + private int|array|null $configPhpVersion, private FileHelper $fileHelper, private array $composerAutoloaderProjectPaths, private array $allConfigFiles, + private ComposerPhpVersionFactory $composerPhpVersionFactory, ) { } @@ -48,11 +53,45 @@ public function print(Output $output): void 'PHP runtime version: %s', $phpRuntimeVersion->getVersionString(), )); - $output->writeLineFormatted(sprintf( - 'PHP version for analysis: %s (from %s)', - $this->phpVersion->getVersionString(), - $this->phpVersion->getSourceLabel(), - )); + + 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( diff --git a/src/File/CouldNotReadFileException.php b/src/File/CouldNotReadFileException.php index 5803fd53df..63d5a2e41d 100644 --- a/src/File/CouldNotReadFileException.php +++ b/src/File/CouldNotReadFileException.php @@ -5,7 +5,7 @@ use PHPStan\AnalysedCodeException; use function sprintf; -class CouldNotReadFileException extends AnalysedCodeException +final class CouldNotReadFileException extends AnalysedCodeException { public function __construct(string $fileName) diff --git a/src/File/CouldNotWriteFileException.php b/src/File/CouldNotWriteFileException.php index 0fffa54dcf..72e00464b7 100644 --- a/src/File/CouldNotWriteFileException.php +++ b/src/File/CouldNotWriteFileException.php @@ -5,7 +5,7 @@ use PHPStan\AnalysedCodeException; use function sprintf; -class CouldNotWriteFileException extends AnalysedCodeException +final class CouldNotWriteFileException extends AnalysedCodeException { public function __construct(string $fileName, string $error) diff --git a/src/File/FileExcluder.php b/src/File/FileExcluder.php index 568f7f1bbb..57a2cee974 100644 --- a/src/File/FileExcluder.php +++ b/src/File/FileExcluder.php @@ -2,6 +2,7 @@ namespace PHPStan\File; +use PHPStan\DependencyInjection\GenerateFactory; use function fnmatch; use function in_array; use function is_dir; @@ -14,7 +15,8 @@ use const FNM_CASEFOLD; use const FNM_NOESCAPE; -class FileExcluder +#[GenerateFactory(interface: FileExcluderRawFactory::class)] +final class FileExcluder { /** @@ -50,9 +52,8 @@ class FileExcluder * @param string[] $analyseExcludes */ public function __construct( - FileHelper $fileHelper, + private FileHelper $fileHelper, array $analyseExcludes, - private bool $noImplicitWildcard, ) { foreach ($analyseExcludes as $exclude) { @@ -68,18 +69,14 @@ public function __construct( if (self::isFnmatchPattern($normalized)) { $this->fnmatchAnalyseExcludes[] = $normalized; } else { - if ($this->noImplicitWildcard) { - if (is_file($normalized)) { - $this->literalAnalyseFilesExcludes[] = $normalized; - } elseif (is_dir($normalized)) { - if (!$trailingDirSeparator) { - $normalized .= DIRECTORY_SEPARATOR; - } - - $this->literalAnalyseDirectoryExcludes[] = $normalized; + if (is_file($normalized)) { + $this->literalAnalyseFilesExcludes[] = $normalized; + } elseif (is_dir($normalized)) { + if (!$trailingDirSeparator) { + $normalized .= DIRECTORY_SEPARATOR; } - } else { - $this->literalAnalyseExcludes[] = $fileHelper->absolutizePath($normalized); + + $this->literalAnalyseDirectoryExcludes[] = $normalized; } } } @@ -94,21 +91,21 @@ public function __construct( public function isExcludedFromAnalysing(string $file): bool { + $file = $this->fileHelper->normalizePath($file); + foreach ($this->literalAnalyseExcludes as $exclude) { if (str_starts_with($file, $exclude)) { return true; } } - if ($this->noImplicitWildcard) { - foreach ($this->literalAnalyseDirectoryExcludes as $exclude) { - if (str_starts_with($file, $exclude)) { - return true; - } + foreach ($this->literalAnalyseDirectoryExcludes as $exclude) { + if (str_starts_with($file, $exclude)) { + return true; } - foreach ($this->literalAnalyseFilesExcludes as $exclude) { - if ($file === $exclude) { - return true; - } + } + foreach ($this->literalAnalyseFilesExcludes as $exclude) { + if ($file === $exclude) { + return true; } } foreach ($this->fnmatchAnalyseExcludes as $exclude) { diff --git a/src/File/FileExcluderFactory.php b/src/File/FileExcluderFactory.php index 26976a3dfb..71c3c3b598 100644 --- a/src/File/FileExcluderFactory.php +++ b/src/File/FileExcluderFactory.php @@ -2,32 +2,30 @@ namespace PHPStan\File; +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; -class FileExcluderFactory +#[AutowiredService] +final class FileExcluderFactory { /** - * @param string[] $obsoleteExcludesAnalyse - * @param array{analyse?: array, analyseAndScan?: array}|null $excludePaths + * @param array{analyse?: array, analyseAndScan?: array} $excludePaths */ public function __construct( private FileExcluderRawFactory $fileExcluderRawFactory, - private array $obsoleteExcludesAnalyse, - private ?array $excludePaths, + #[AutowiredParameter] + private array $excludePaths, ) { } public function createAnalyseFileExcluder(): FileExcluder { - if ($this->excludePaths === null) { - return $this->fileExcluderRawFactory->create($this->obsoleteExcludesAnalyse); - } - $paths = []; if (array_key_exists('analyse', $this->excludePaths)) { $paths = $this->excludePaths['analyse']; @@ -41,10 +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/FileFinder.php b/src/File/FileFinder.php index 6ca9db6ddd..34ab2c5a16 100644 --- a/src/File/FileFinder.php +++ b/src/File/FileFinder.php @@ -10,7 +10,7 @@ use function implode; use function is_file; -class FileFinder +final class FileFinder { /** diff --git a/src/File/FileFinderResult.php b/src/File/FileFinderResult.php index d88bcbb84c..7ae7634c96 100644 --- a/src/File/FileFinderResult.php +++ b/src/File/FileFinderResult.php @@ -2,7 +2,7 @@ namespace PHPStan\File; -class FileFinderResult +final class FileFinderResult { /** diff --git a/src/File/FileHelper.php b/src/File/FileHelper.php index 06bca961f3..8c6b32ec8d 100644 --- a/src/File/FileHelper.php +++ b/src/File/FileHelper.php @@ -3,6 +3,8 @@ 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; @@ -18,12 +20,16 @@ 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); } diff --git a/src/File/FileMonitor.php b/src/File/FileMonitor.php index 26e27454ff..af812b9bbf 100644 --- a/src/File/FileMonitor.php +++ b/src/File/FileMonitor.php @@ -2,52 +2,78 @@ namespace PHPStan\File; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\ShouldNotHappenException; +use function array_diff; use function array_key_exists; use function array_keys; -use function count; +use function array_merge; +use function array_unique; +use function is_dir; +use function is_file; use function sha1_file; -class FileMonitor +#[AutowiredService] +final class FileMonitor { /** @var array|null */ private ?array $fileHashes = null; /** @var array|null */ - private ?array $paths = null; + private ?array $filePaths = null; - public function __construct(private 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, + ) { } /** - * @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_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) { + 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) { + foreach (array_merge($finderResult->getFiles(), $this->filePaths, $this->getScannedFiles($finderResult->getFiles())) as $filePath) { if (!array_key_exists($filePath, $oldFileHashes)) { $newFiles[] = $filePath; $fileHashes[$filePath] = $this->getFileHash($filePath); @@ -75,7 +101,6 @@ public function getChanges(): FileMonitorResult $newFiles, $changedFiles, $deletedFiles, - count($fileHashes), ); } @@ -90,4 +115,32 @@ private function getFileHash(string $filePath): string 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 940c21e965..f76ae9dde4 100644 --- a/src/File/FileMonitorResult.php +++ b/src/File/FileMonitorResult.php @@ -4,7 +4,7 @@ use function count; -class FileMonitorResult +final class FileMonitorResult { /** @@ -16,7 +16,6 @@ public function __construct( private array $newFiles, private array $changedFiles, private array $deletedFiles, - private int $totalFilesCount, ) { } @@ -36,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 46f8933916..7f5a8dc369 100644 --- a/src/File/FileReader.php +++ b/src/File/FileReader.php @@ -5,7 +5,7 @@ use function file_get_contents; use function stream_resolve_include_path; -class FileReader +final class FileReader { /** diff --git a/src/File/FileWriter.php b/src/File/FileWriter.php index 2a40c35bb1..b3659d9536 100644 --- a/src/File/FileWriter.php +++ b/src/File/FileWriter.php @@ -5,7 +5,7 @@ use function error_get_last; use function file_put_contents; -class FileWriter +final class FileWriter { public static function write(string $fileName, string $contents): void diff --git a/src/File/FuzzyRelativePathHelper.php b/src/File/FuzzyRelativePathHelper.php index dc2d44e505..5e757df06f 100644 --- a/src/File/FuzzyRelativePathHelper.php +++ b/src/File/FuzzyRelativePathHelper.php @@ -2,6 +2,8 @@ namespace PHPStan\File; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use function count; use function explode; use function implode; @@ -14,7 +16,8 @@ use function substr; use const DIRECTORY_SEPARATOR; -class FuzzyRelativePathHelper implements RelativePathHelper +#[AutowiredService(name: 'relativePathHelper', as: RelativePathHelper::class)] +final class FuzzyRelativePathHelper implements RelativePathHelper { private string $directorySeparator; @@ -26,8 +29,11 @@ class FuzzyRelativePathHelper implements RelativePathHelper * @param non-empty-string|null $directorySeparator */ public function __construct( + #[AutowiredParameter(ref: '@parentDirectoryRelativePathHelper')] private RelativePathHelper $fallbackRelativePathHelper, + #[AutowiredParameter] string $currentWorkingDirectory, + #[AutowiredParameter] array $analysedPaths, ?string $directorySeparator = null, ) diff --git a/src/File/NullRelativePathHelper.php b/src/File/NullRelativePathHelper.php index 1556984a90..5e5a07dc62 100644 --- a/src/File/NullRelativePathHelper.php +++ b/src/File/NullRelativePathHelper.php @@ -2,7 +2,7 @@ namespace PHPStan\File; -class NullRelativePathHelper implements RelativePathHelper +final class NullRelativePathHelper implements RelativePathHelper { public function getRelativePath(string $filename): string diff --git a/src/File/ParentDirectoryRelativePathHelper.php b/src/File/ParentDirectoryRelativePathHelper.php index 218d4597fc..bea0edb077 100644 --- a/src/File/ParentDirectoryRelativePathHelper.php +++ b/src/File/ParentDirectoryRelativePathHelper.php @@ -2,6 +2,8 @@ namespace PHPStan\File; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\NonAutowiredService; use PHPStan\ShouldNotHappenException; use function array_fill; use function array_merge; @@ -14,10 +16,14 @@ use function substr; use function trim; -class ParentDirectoryRelativePathHelper implements RelativePathHelper +#[NonAutowiredService(name: 'parentDirectoryRelativePathHelper')] +final class ParentDirectoryRelativePathHelper implements RelativePathHelper { - public function __construct(private string $parentDirectory) + public function __construct( + #[AutowiredParameter(ref: '%currentWorkingDirectory%')] + private string $parentDirectory, + ) { } diff --git a/src/File/PathNotFoundException.php b/src/File/PathNotFoundException.php index fb8f4dd191..9dc613ccb7 100644 --- a/src/File/PathNotFoundException.php +++ b/src/File/PathNotFoundException.php @@ -5,17 +5,12 @@ use Exception; use function sprintf; -class PathNotFoundException extends Exception +final class PathNotFoundException extends Exception { - public function __construct(private string $path) + public function __construct(string $path) { parent::__construct(sprintf('Path %s does not exist', $path)); } - public function getPath(): string - { - return $this->path; - } - } diff --git a/src/File/SimpleRelativePathHelper.php b/src/File/SimpleRelativePathHelper.php index 854f584b47..22c6221871 100644 --- a/src/File/SimpleRelativePathHelper.php +++ b/src/File/SimpleRelativePathHelper.php @@ -2,15 +2,21 @@ namespace PHPStan\File; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\NonAutowiredService; use function str_replace; use function str_starts_with; use function strlen; use function substr; -class SimpleRelativePathHelper implements RelativePathHelper +#[NonAutowiredService(name: 'simpleRelativePathHelper')] +final class SimpleRelativePathHelper implements RelativePathHelper { - public function __construct(private string $currentWorkingDirectory) + public function __construct( + #[AutowiredParameter(ref: '%currentWorkingDirectory%')] + private string $currentWorkingDirectory, + ) { } diff --git a/src/File/SystemAgnosticSimpleRelativePathHelper.php b/src/File/SystemAgnosticSimpleRelativePathHelper.php index d040956fb2..bd4ac68d67 100644 --- a/src/File/SystemAgnosticSimpleRelativePathHelper.php +++ b/src/File/SystemAgnosticSimpleRelativePathHelper.php @@ -6,7 +6,7 @@ use function strlen; use function substr; -class SystemAgnosticSimpleRelativePathHelper implements RelativePathHelper +final class SystemAgnosticSimpleRelativePathHelper implements RelativePathHelper { public function __construct(private FileHelper $fileHelper) 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'); + $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 48a852c0a3..3279501f51 100644 --- a/src/Internal/BytesHelper.php +++ b/src/Internal/BytesHelper.php @@ -7,7 +7,7 @@ use function end; use function round; -class BytesHelper +final class BytesHelper { public static function bytes(int $bytes): string diff --git a/src/Internal/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/SprintfHelper.php b/src/Internal/SprintfHelper.php index 30d08500fa..6938c898f1 100644 --- a/src/Internal/SprintfHelper.php +++ b/src/Internal/SprintfHelper.php @@ -4,7 +4,7 @@ use function str_replace; -class SprintfHelper +final class SprintfHelper { public static function escapeFormatString(string $format): string diff --git a/src/Node/AnonymousClassNode.php b/src/Node/AnonymousClassNode.php index b5d8333496..afed122f56 100644 --- a/src/Node/AnonymousClassNode.php +++ b/src/Node/AnonymousClassNode.php @@ -2,6 +2,7 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Stmt\Class_; /** @@ -24,6 +25,7 @@ public static function createFromClassNode(Class_ $node): self ); } + #[Override] public function isAnonymous(): bool { return true; diff --git a/src/Node/BooleanAndNode.php b/src/Node/BooleanAndNode.php index 55573e7eb8..cf88a8b1bb 100644 --- a/src/Node/BooleanAndNode.php +++ b/src/Node/BooleanAndNode.php @@ -2,13 +2,16 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\BooleanAnd; use PhpParser\Node\Expr\BinaryOp\LogicalAnd; use PHPStan\Analyser\Scope; -/** @api */ -class BooleanAndNode extends Expr implements VirtualNode +/** + * @api + */ +final class BooleanAndNode extends Expr implements VirtualNode { public function __construct(private BooleanAnd|LogicalAnd $originalNode, private Scope $rightScope) @@ -29,6 +32,7 @@ public function getRightScope(): Scope return $this->rightScope; } + #[Override] public function getType(): string { return 'PHPStan_Node_BooleanAndNode'; @@ -37,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 0c1523e60b..49e1e8edd9 100644 --- a/src/Node/BooleanOrNode.php +++ b/src/Node/BooleanOrNode.php @@ -2,13 +2,16 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\BooleanOr; use PhpParser\Node\Expr\BinaryOp\LogicalOr; use PHPStan\Analyser\Scope; -/** @api */ -class BooleanOrNode extends Expr implements VirtualNode +/** + * @api + */ +final class BooleanOrNode extends Expr implements VirtualNode { public function __construct(private BooleanOr|LogicalOr $originalNode, private Scope $rightScope) @@ -29,6 +32,7 @@ public function getRightScope(): Scope return $this->rightScope; } + #[Override] public function getType(): string { return 'PHPStan_Node_BooleanOrNode'; @@ -37,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 8a824778bc..4bbfe497f7 100644 --- a/src/Node/BreaklessWhileLoopNode.php +++ b/src/Node/BreaklessWhileLoopNode.php @@ -2,12 +2,15 @@ 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 { /** @@ -31,6 +34,7 @@ public function getExitPoints(): array return $this->exitPoints; } + #[Override] public function getType(): string { return 'PHPStan_Node_BreaklessWhileLoop'; @@ -39,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 007fa83218..9288d863c1 100644 --- a/src/Node/CatchWithUnthrownExceptionNode.php +++ b/src/Node/CatchWithUnthrownExceptionNode.php @@ -2,12 +2,15 @@ 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 { public function __construct(private Catch_ $originalNode, private Type $caughtType, private Type $originalCaughtType) @@ -30,6 +33,7 @@ public function getOriginalCaughtType(): Type return $this->originalCaughtType; } + #[Override] public function getType(): string { return 'PHPStan_Node_CatchWithUnthrownExceptionNode'; @@ -38,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 dee7d0019a..fbc120199d 100644 --- a/src/Node/ClassConstantsNode.php +++ b/src/Node/ClassConstantsNode.php @@ -2,14 +2,17 @@ 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 { /** @@ -42,6 +45,7 @@ public function getFetches(): array return $this->fetches; } + #[Override] public function getType(): string { return 'PHPStan_Node_ClassConstantsNode'; @@ -50,6 +54,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/ClassMethod.php b/src/Node/ClassMethod.php index 6ecfaeead8..2aec877cf5 100644 --- a/src/Node/ClassMethod.php +++ b/src/Node/ClassMethod.php @@ -2,30 +2,22 @@ namespace PHPStan\Node; -use PhpParser\Node\Stmt\ClassMethod as PhpParserClassMethod; - -/** @api */ -class ClassMethod extends PhpParserClassMethod +/** + * @api + */ +final class ClassMethod { public function __construct( - \PhpParser\Node\Stmt\ClassMethod $node, + private \PhpParser\Node\Stmt\ClassMethod $node, private bool $isDeclaredInTrait, ) { - parent::__construct($node->name, [ - 'flags' => $node->flags, - 'byRef' => $node->byRef, - 'params' => $node->params, - 'returnType' => $node->returnType, - 'stmts' => $node->stmts, - 'attrGroups' => $node->attrGroups, - ], $node->attributes); } - public function getNode(): PhpParserClassMethod + public function getNode(): \PhpParser\Node\Stmt\ClassMethod { - return $this; + return $this->node; } public function isDeclaredInTrait(): bool diff --git a/src/Node/ClassMethodsNode.php b/src/Node/ClassMethodsNode.php index e02620532b..bdb30ae3d2 100644 --- a/src/Node/ClassMethodsNode.php +++ b/src/Node/ClassMethodsNode.php @@ -2,13 +2,16 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Stmt\ClassLike; 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 { /** @@ -41,6 +44,7 @@ public function getMethodCalls(): array return $this->methodCalls; } + #[Override] public function getType(): string { return 'PHPStan_Node_ClassMethodsNode'; @@ -49,6 +53,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/ClassPropertiesNode.php b/src/Node/ClassPropertiesNode.php index 8d9659a68b..a966e3038a 100644 --- a/src/Node/ClassPropertiesNode.php +++ b/src/Node/ClassPropertiesNode.php @@ -2,6 +2,7 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\PropertyFetch; @@ -13,11 +14,11 @@ 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\Rules\Properties\ReadWritePropertiesExtensionProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\NeverType; @@ -28,8 +29,10 @@ use function in_array; use function strtolower; -/** @api */ -class ClassPropertiesNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class ClassPropertiesNode extends NodeAbstract implements VirtualNode { /** @@ -37,6 +40,7 @@ class ClassPropertiesNode extends NodeAbstract implements VirtualNode * @param array $propertyUsages * @param array $methodCalls * @param array $returnStatementNodes + * @param list $propertyAssigns */ public function __construct( private ClassLike $class, @@ -45,6 +49,7 @@ public function __construct( private array $propertyUsages, private array $methodCalls, private array $returnStatementNodes, + private array $propertyAssigns, private ClassReflection $classReflection, ) { @@ -72,6 +77,7 @@ public function getPropertyUsages(): array return $this->propertyUsages; } + #[Override] public function getType(): string { return 'PHPStan_Node_ClassPropertiesNode'; @@ -80,6 +86,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; @@ -92,13 +99,11 @@ public function getClassReflection(): ClassReflection /** * @param string[] $constructors - * @param ReadWritePropertiesExtension[]|null $extensions * @return array{array, array, array} */ public function getUninitializedProperties( Scope $scope, array $constructors, - ?array $extensions = null, ): array { if (!$this->getClass() instanceof Class_) { @@ -110,14 +115,15 @@ public function getUninitializedProperties( $originalProperties = []; $initialInitializedProperties = []; $initializedProperties = []; - if ($extensions === null) { - $extensions = $this->readWritePropertiesExtensionProvider->getExtensions(); - } + $extensions = $this->readWritePropertiesExtensionProvider->getExtensions(); $initializedViaExtension = []; foreach ($this->getProperties() as $property) { if ($property->isStatic()) { continue; } + if ($property->isAbstract()) { + continue; + } if ($property->getNativeType() === null) { continue; } @@ -128,6 +134,9 @@ public function getUninitializedProperties( $is = TrinaryLogic::createFromBoolean($property->isPromoted() && !$property->isPromotedFromTrait()); if (!$is->yes() && $classReflection->hasNativeProperty($property->getName())) { $propertyReflection = $classReflection->getNativeProperty($property->getName()); + if ($propertyReflection->isVirtual()->yes()) { + continue; + } foreach ($extensions as $extension) { if (!$extension->isInitialized($propertyReflection, $property->getName())) { @@ -265,9 +274,6 @@ private function collectUninitializedProperties(array $constructors, array $unin $statementResult = $executionEnd->getStatementResult(); $endNode = $executionEnd->getNode(); if ($statementResult->isAlwaysTerminating()) { - if ($endNode instanceof Node\Stmt\Throw_) { - continue; - } if ($endNode instanceof Node\Stmt\Expression) { $exprType = $statementResult->getScope()->getType($endNode->expr); if ($exprType instanceof NeverType && $exprType->isExplicit()) { @@ -401,4 +407,12 @@ private function getInitializedProperties(Scope $scope, array $initialInitialize return $initialInitializedProperties; } + /** + * @return list + */ + public function getPropertyAssigns(): array + { + return $this->propertyAssigns; + } + } diff --git a/src/Node/ClassPropertyNode.php b/src/Node/ClassPropertyNode.php index a7c81ff9d4..89590f1472 100644 --- a/src/Node/ClassPropertyNode.php +++ b/src/Node/ClassPropertyNode.php @@ -2,29 +2,33 @@ 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 { + /** + * @param non-empty-string $name + */ public function __construct( private string $name, private int $flags, - private Identifier|Name|Node\ComplexType|null $type, + private ?Type $type, private ?Expr $default, private ?string $phpDoc, private ?Type $phpDocType, private bool $isPromoted, private bool $isPromotedFromTrait, - Node $originalNode, + private Node\Stmt\Property|Node\Param $originalNode, private bool $isReadonlyByPhpDoc, private bool $isDeclaredInTrait, private bool $isReadonlyClass, @@ -35,6 +39,7 @@ public function __construct( parent::__construct($originalNode->getAttributes()); } + /** @return non-empty-string */ public function getName(): string { return $this->name; @@ -72,28 +77,33 @@ public function getPhpDocType(): ?Type 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) || $this->isReadonlyClass; + return (bool) ($this->flags & Modifiers::READONLY) || $this->isReadonlyClass; } public function isReadOnlyByPhpDoc(): bool @@ -111,12 +121,22 @@ 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 @@ -124,6 +144,7 @@ public function getClassReflection(): ClassReflection return $this->classReflection; } + #[Override] public function getType(): string { return 'PHPStan_Node_ClassPropertyNode'; @@ -132,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 6b8b2438c3..a2bb6889d5 100644 --- a/src/Node/ClassStatementsGatherer.php +++ b/src/Node/ClassStatementsGatherer.php @@ -13,6 +13,7 @@ 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; @@ -23,7 +24,7 @@ use function in_array; use function strtolower; -class ClassStatementsGatherer +final class ClassStatementsGatherer { private const PROPERTY_ENUMERATING_FUNCTIONS = [ @@ -55,6 +56,9 @@ class ClassStatementsGatherer /** @var array */ private array $returnStatementNodes = []; + /** @var list */ + private array $propertyAssigns = []; + /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ @@ -122,6 +126,14 @@ 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; @@ -189,6 +201,7 @@ private function gatherNodes(Node $node, Scope $scope): void } if ($node instanceof PropertyAssignNode) { $this->propertyUsages[] = new PropertyWrite($node->getPropertyFetch(), $scope, false); + $this->propertyAssigns[] = new PropertyAssign($node, $scope); return; } if (!$node instanceof Expr) { @@ -208,9 +221,6 @@ private function gatherNodes(Node $node, Scope $scope): void $this->propertyUsages[] = new PropertyWrite($node->expr, $scope, false); return; } - if ($node instanceof Node\Scalar\EncapsedStringPart) { - return; - } if ($node instanceof FunctionCallableNode) { $node = $node->getOriginalNode(); } elseif ($node instanceof InstantiationCallableNode) { @@ -249,6 +259,9 @@ private function tryToApplyPropertyReads(Expr\FuncCall $node, Scope $scope): voi 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, @@ -272,6 +285,9 @@ private function tryToApplyPropertyWritesFromAncestorConstructor(StaticCall $anc 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, diff --git a/src/Node/ClosureReturnStatementsNode.php b/src/Node/ClosureReturnStatementsNode.php index 0a20615ca8..87b63a60a4 100644 --- a/src/Node/ClosureReturnStatementsNode.php +++ b/src/Node/ClosureReturnStatementsNode.php @@ -2,6 +2,7 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\Yield_; @@ -11,8 +12,10 @@ use PHPStan\Analyser\StatementResult; use function count; -/** @api */ -class ClosureReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode +/** + * @api + */ +final class ClosureReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode { private Node\Expr\Closure $closureExpr; @@ -81,6 +84,7 @@ public function returnsByRef(): bool return $this->closureExpr->byRef; } + #[Override] public function getType(): string { return 'PHPStan_Node_ClosureReturnStatementsNode'; @@ -89,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 index 8f7684e1ec..14a12815fe 100644 --- a/src/Node/CollectedDataNode.php +++ b/src/Node/CollectedDataNode.php @@ -2,18 +2,21 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node; use PhpParser\NodeAbstract; use PHPStan\Collectors\CollectedData; use PHPStan\Collectors\Collector; -use function array_key_exists; -/** @api */ -class CollectedDataNode extends NodeAbstract +/** + * @api + * @phpstan-import-type CollectorData from CollectedData + */ +final class CollectedDataNode extends NodeAbstract implements VirtualNode { /** - * @param CollectedData[] $collectedData + * @param CollectorData $collectedData */ public function __construct(private array $collectedData, private bool $onlyFiles) { @@ -29,17 +32,14 @@ public function __construct(private array $collectedData, private bool $onlyFile public function get(string $collectorType): array { $result = []; - foreach ($this->collectedData as $collectedData) { - if ($collectedData->getCollectorType() !== $collectorType) { + foreach ($this->collectedData as $filePath => $collectedDataPerCollector) { + if (!isset($collectedDataPerCollector[$collectorType])) { continue; } - $filePath = $collectedData->getFilePath(); - if (!array_key_exists($filePath, $result)) { - $result[$filePath] = []; + foreach ($collectedDataPerCollector[$collectorType] as $rawData) { + $result[$filePath][] = $rawData; } - - $result[$filePath][] = $collectedData->getData(); } return $result; @@ -55,6 +55,7 @@ public function isOnlyFilesAnalysis(): bool return $this->onlyFiles; } + #[Override] public function getType(): string { return 'PHPStan_Node_CollectedDataNode'; @@ -63,6 +64,7 @@ public function getType(): string /** * @return array{} */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/Constant/ClassConstantFetch.php b/src/Node/Constant/ClassConstantFetch.php index 30129007d0..bda533900b 100644 --- a/src/Node/Constant/ClassConstantFetch.php +++ b/src/Node/Constant/ClassConstantFetch.php @@ -5,8 +5,10 @@ use PhpParser\Node\Expr\ClassConstFetch; use PHPStan\Analyser\Scope; -/** @api */ -class ClassConstantFetch +/** + * @api + */ +final class ClassConstantFetch { public function __construct(private ClassConstFetch $node, private Scope $scope) diff --git a/src/Node/DoWhileLoopConditionNode.php b/src/Node/DoWhileLoopConditionNode.php index c6cbc86572..5e589416eb 100644 --- a/src/Node/DoWhileLoopConditionNode.php +++ b/src/Node/DoWhileLoopConditionNode.php @@ -2,11 +2,12 @@ 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 { /** @@ -30,6 +31,7 @@ public function getExitPoints(): array return $this->exitPoints; } + #[Override] public function getType(): string { return 'PHPStan_Node_ClosureReturnStatementsNode'; @@ -38,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 c0a0f52dd8..4eb9699850 100644 --- a/src/Node/ExecutionEndNode.php +++ b/src/Node/ExecutionEndNode.php @@ -2,12 +2,15 @@ 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 { public function __construct( @@ -34,6 +37,7 @@ public function hasNativeReturnTypehint(): bool return $this->hasNativeReturnTypehint; } + #[Override] public function getType(): string { return 'PHPStan_Node_ExecutionEndNode'; @@ -42,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 index 049ad46a6b..71f40b9724 100644 --- a/src/Node/Expr/AlwaysRememberedExpr.php +++ b/src/Node/Expr/AlwaysRememberedExpr.php @@ -2,11 +2,12 @@ namespace PHPStan\Node\Expr; +use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; use PHPStan\Type\Type; -class AlwaysRememberedExpr extends Expr implements VirtualNode +final class AlwaysRememberedExpr extends Expr implements VirtualNode { public function __construct(public Expr $expr, private Type $type, private Type $nativeType) @@ -29,6 +30,7 @@ public function getNativeExprType(): Type return $this->nativeType; } + #[Override] public function getType(): string { return 'PHPStan_Node_AlwaysRememberedExpr'; @@ -37,6 +39,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return ['expr']; diff --git a/src/Node/Expr/ExistingArrayDimFetch.php b/src/Node/Expr/ExistingArrayDimFetch.php index 80412b4fa3..95c5aabd5c 100644 --- a/src/Node/Expr/ExistingArrayDimFetch.php +++ b/src/Node/Expr/ExistingArrayDimFetch.php @@ -2,10 +2,11 @@ namespace PHPStan\Node\Expr; +use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; -class ExistingArrayDimFetch extends Expr implements VirtualNode +final class ExistingArrayDimFetch extends Expr implements VirtualNode { public function __construct(private Expr $var, private Expr $dim) @@ -23,6 +24,7 @@ public function getDim(): Expr return $this->dim; } + #[Override] public function getType(): string { return 'PHPStan_Node_ExistingArrayDimFetch'; @@ -31,6 +33,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/Expr/GetIterableKeyTypeExpr.php b/src/Node/Expr/GetIterableKeyTypeExpr.php index 40301cc29f..6073173053 100644 --- a/src/Node/Expr/GetIterableKeyTypeExpr.php +++ b/src/Node/Expr/GetIterableKeyTypeExpr.php @@ -2,10 +2,11 @@ namespace PHPStan\Node\Expr; +use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; -class GetIterableKeyTypeExpr extends Expr implements VirtualNode +final class GetIterableKeyTypeExpr extends Expr implements VirtualNode { public function __construct(private Expr $expr) @@ -18,6 +19,7 @@ public function getExpr(): Expr return $this->expr; } + #[Override] public function getType(): string { return 'PHPStan_Node_GetIterableKeyTypeExpr'; @@ -26,6 +28,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/Expr/GetIterableValueTypeExpr.php b/src/Node/Expr/GetIterableValueTypeExpr.php index 2fd920f21a..642eb4870e 100644 --- a/src/Node/Expr/GetIterableValueTypeExpr.php +++ b/src/Node/Expr/GetIterableValueTypeExpr.php @@ -2,10 +2,11 @@ namespace PHPStan\Node\Expr; +use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; -class GetIterableValueTypeExpr extends Expr implements VirtualNode +final class GetIterableValueTypeExpr extends Expr implements VirtualNode { public function __construct(private Expr $expr) @@ -18,6 +19,7 @@ public function getExpr(): Expr return $this->expr; } + #[Override] public function getType(): string { return 'PHPStan_Node_GetIterableValueTypeExpr'; @@ -26,6 +28,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/Expr/GetOffsetValueTypeExpr.php b/src/Node/Expr/GetOffsetValueTypeExpr.php index ed7b33a124..3822685555 100644 --- a/src/Node/Expr/GetOffsetValueTypeExpr.php +++ b/src/Node/Expr/GetOffsetValueTypeExpr.php @@ -2,10 +2,11 @@ namespace PHPStan\Node\Expr; +use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; -class GetOffsetValueTypeExpr extends Expr implements VirtualNode +final class GetOffsetValueTypeExpr extends Expr implements VirtualNode { public function __construct(private Expr $var, private Expr $dim) @@ -23,6 +24,7 @@ public function getDim(): Expr return $this->dim; } + #[Override] public function getType(): string { return 'PHPStan_Node_GetOffsetValueTypeExpr'; @@ -31,6 +33,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/Expr/OriginalPropertyTypeExpr.php b/src/Node/Expr/OriginalPropertyTypeExpr.php index 2250a8158b..d662dbe488 100644 --- a/src/Node/Expr/OriginalPropertyTypeExpr.php +++ b/src/Node/Expr/OriginalPropertyTypeExpr.php @@ -2,10 +2,11 @@ namespace PHPStan\Node\Expr; +use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; -class OriginalPropertyTypeExpr extends Expr implements VirtualNode +final class OriginalPropertyTypeExpr extends Expr implements VirtualNode { public function __construct(private Expr\PropertyFetch|Expr\StaticPropertyFetch $propertyFetch) @@ -18,6 +19,7 @@ public function getPropertyFetch(): Expr\PropertyFetch|Expr\StaticPropertyFetch return $this->propertyFetch; } + #[Override] public function getType(): string { return 'PHPStan_Node_OriginalPropertyTypeExpr'; @@ -26,6 +28,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/Expr/ParameterVariableOriginalValueExpr.php b/src/Node/Expr/ParameterVariableOriginalValueExpr.php index 277d51c040..fa1315e9b8 100644 --- a/src/Node/Expr/ParameterVariableOriginalValueExpr.php +++ b/src/Node/Expr/ParameterVariableOriginalValueExpr.php @@ -2,10 +2,11 @@ namespace PHPStan\Node\Expr; +use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; -class ParameterVariableOriginalValueExpr extends Expr implements VirtualNode +final class ParameterVariableOriginalValueExpr extends Expr implements VirtualNode { public function __construct(private string $variableName) @@ -18,6 +19,7 @@ public function getVariableName(): string return $this->variableName; } + #[Override] public function getType(): string { return 'PHPStan_Node_ParameterVariableOriginalValueExpr'; @@ -26,6 +28,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/Expr/PropertyInitializationExpr.php b/src/Node/Expr/PropertyInitializationExpr.php index 942fa08d5c..539d928f4a 100644 --- a/src/Node/Expr/PropertyInitializationExpr.php +++ b/src/Node/Expr/PropertyInitializationExpr.php @@ -2,10 +2,11 @@ namespace PHPStan\Node\Expr; +use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; -class PropertyInitializationExpr extends Expr implements VirtualNode +final class PropertyInitializationExpr extends Expr implements VirtualNode { public function __construct(private string $propertyName) @@ -18,6 +19,7 @@ public function getPropertyName(): string return $this->propertyName; } + #[Override] public function getType(): string { return 'PHPStan_Node_PropertyInitializationExpr'; @@ -26,6 +28,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/Expr/SetExistingOffsetValueTypeExpr.php b/src/Node/Expr/SetExistingOffsetValueTypeExpr.php index 52eb9a4b37..a152f9f329 100644 --- a/src/Node/Expr/SetExistingOffsetValueTypeExpr.php +++ b/src/Node/Expr/SetExistingOffsetValueTypeExpr.php @@ -2,10 +2,11 @@ namespace PHPStan\Node\Expr; +use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; -class SetExistingOffsetValueTypeExpr extends Expr implements VirtualNode +final class SetExistingOffsetValueTypeExpr extends Expr implements VirtualNode { public function __construct(private Expr $var, private Expr $dim, private Expr $value) @@ -28,6 +29,7 @@ public function getValue(): Expr return $this->value; } + #[Override] public function getType(): string { return 'PHPStan_Node_SetExistingOffsetValueTypeExpr'; @@ -36,6 +38,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/Expr/SetOffsetValueTypeExpr.php b/src/Node/Expr/SetOffsetValueTypeExpr.php index 3e42e13b0c..3317f202de 100644 --- a/src/Node/Expr/SetOffsetValueTypeExpr.php +++ b/src/Node/Expr/SetOffsetValueTypeExpr.php @@ -2,10 +2,11 @@ namespace PHPStan\Node\Expr; +use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; -class SetOffsetValueTypeExpr extends Expr implements VirtualNode +final class SetOffsetValueTypeExpr extends Expr implements VirtualNode { public function __construct(private Expr $var, private ?Expr $dim, private Expr $value) @@ -28,6 +29,7 @@ public function getValue(): Expr return $this->value; } + #[Override] public function getType(): string { return 'PHPStan_Node_SetOffsetValueTypeExpr'; @@ -36,6 +38,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/Expr/TypeExpr.php b/src/Node/Expr/TypeExpr.php index 4adc5be0e3..cc822aed90 100644 --- a/src/Node/Expr/TypeExpr.php +++ b/src/Node/Expr/TypeExpr.php @@ -2,13 +2,18 @@ namespace PHPStan\Node\Expr; +use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; use PHPStan\Type\Type; -class TypeExpr extends Expr implements VirtualNode +/** + * @api + */ +final class TypeExpr extends Expr implements VirtualNode { + /** @api */ public function __construct(private Type $exprType) { parent::__construct(); @@ -19,6 +24,7 @@ public function getExprType(): Type return $this->exprType; } + #[Override] public function getType(): string { return 'PHPStan_Node_TypeExpr'; @@ -27,6 +33,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/Expr/UnsetOffsetExpr.php b/src/Node/Expr/UnsetOffsetExpr.php index 55c81eef92..3fe9c7d09e 100644 --- a/src/Node/Expr/UnsetOffsetExpr.php +++ b/src/Node/Expr/UnsetOffsetExpr.php @@ -2,10 +2,11 @@ namespace PHPStan\Node\Expr; +use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; -class UnsetOffsetExpr extends Expr implements VirtualNode +final class UnsetOffsetExpr extends Expr implements VirtualNode { public function __construct(private Expr $var, private Expr $dim) @@ -23,6 +24,7 @@ public function getDim(): Expr return $this->dim; } + #[Override] public function getType(): string { return 'PHPStan_Node_UnsetOffsetExpr'; @@ -31,6 +33,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/FileNode.php b/src/Node/FileNode.php index 13a6596ced..f8529cd6e0 100644 --- a/src/Node/FileNode.php +++ b/src/Node/FileNode.php @@ -2,11 +2,14 @@ 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 { /** @@ -26,6 +29,7 @@ public function getNodes(): array return $this->nodes; } + #[Override] public function getType(): string { return 'PHPStan_Node_FileNode'; @@ -34,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 a87bdc516c..c6636b5a57 100644 --- a/src/Node/FinallyExitPointsNode.php +++ b/src/Node/FinallyExitPointsNode.php @@ -2,11 +2,14 @@ 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 { /** @@ -34,6 +37,7 @@ public function getTryCatchExitPoints(): array return $this->tryCatchExitPoints; } + #[Override] public function getType(): string { return 'PHPStan_Node_FinallyExitPointsNode'; @@ -42,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 index d43e0228ce..cf7c1ba9fd 100644 --- a/src/Node/FunctionCallableNode.php +++ b/src/Node/FunctionCallableNode.php @@ -2,11 +2,14 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Expr; use PhpParser\Node\Name; -/** @api */ -class FunctionCallableNode extends Expr implements VirtualNode +/** + * @api + */ +final class FunctionCallableNode extends Expr implements VirtualNode { public function __construct(private Name|Expr $name, private Expr\FuncCall $originalNode) @@ -27,6 +30,7 @@ public function getOriginalNode(): Expr\FuncCall return $this->originalNode; } + #[Override] public function getType(): string { return 'PHPStan_Node_FunctionCallableNode'; @@ -35,6 +39,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/FunctionReturnStatementsNode.php b/src/Node/FunctionReturnStatementsNode.php index 86df35e24c..cbacab6749 100644 --- a/src/Node/FunctionReturnStatementsNode.php +++ b/src/Node/FunctionReturnStatementsNode.php @@ -2,6 +2,7 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Expr\Yield_; use PhpParser\Node\Expr\YieldFrom; use PhpParser\Node\Stmt; @@ -9,11 +10,13 @@ use PhpParser\NodeAbstract; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; -use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use function count; -/** @api */ -class FunctionReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode +/** + * @api + */ +final class FunctionReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode { /** @@ -29,7 +32,7 @@ public function __construct( private StatementResult $statementResult, private array $executionEnds, private array $impurePoints, - private FunctionReflection $functionReflection, + private PhpFunctionFromParserNodeReflection $functionReflection, ) { parent::__construct($function->getAttributes()); @@ -75,6 +78,7 @@ public function isGenerator(): bool return count($this->yieldStatements) > 0; } + #[Override] public function getType(): string { return 'PHPStan_Node_FunctionReturnStatementsNode'; @@ -83,12 +87,13 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; } - public function getFunctionReflection(): FunctionReflection + public function getFunctionReflection(): PhpFunctionFromParserNodeReflection { return $this->functionReflection; } diff --git a/src/Node/InArrowFunctionNode.php b/src/Node/InArrowFunctionNode.php index 7716f332b4..6876978cec 100644 --- a/src/Node/InArrowFunctionNode.php +++ b/src/Node/InArrowFunctionNode.php @@ -2,13 +2,16 @@ 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 Node\Expr\ArrowFunction $originalNode; @@ -29,6 +32,7 @@ public function getOriginalNode(): Node\Expr\ArrowFunction return $this->originalNode; } + #[Override] public function getType(): string { return 'PHPStan_Node_InArrowFunctionNode'; @@ -37,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 da1d8a09b7..6547b7a12a 100644 --- a/src/Node/InClassMethodNode.php +++ b/src/Node/InClassMethodNode.php @@ -2,12 +2,15 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; -/** @api */ -class InClassMethodNode extends Node\Stmt implements VirtualNode +/** + * @api + */ +final class InClassMethodNode extends Node\Stmt implements VirtualNode { public function __construct( @@ -34,6 +37,7 @@ public function getOriginalNode(): Node\Stmt\ClassMethod return $this->originalNode; } + #[Override] public function getType(): string { return 'PHPStan_Stmt_InClassMethodNode'; @@ -42,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 23310aaec0..69c046be1e 100644 --- a/src/Node/InClassNode.php +++ b/src/Node/InClassNode.php @@ -2,12 +2,15 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node; use PhpParser\Node\Stmt\ClassLike; use PHPStan\Reflection\ClassReflection; -/** @api */ -class InClassNode extends Node\Stmt implements VirtualNode +/** + * @api + */ +final class InClassNode extends Node\Stmt implements VirtualNode { public function __construct(private ClassLike $originalNode, private ClassReflection $classReflection) @@ -25,6 +28,7 @@ public function getClassReflection(): ClassReflection return $this->classReflection; } + #[Override] public function getType(): string { return 'PHPStan_Stmt_InClassNode'; @@ -33,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 8882ebd0e3..15ee9acd59 100644 --- a/src/Node/InClosureNode.php +++ b/src/Node/InClosureNode.php @@ -2,13 +2,16 @@ 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 Node\Expr\Closure $originalNode; @@ -29,6 +32,7 @@ public function getOriginalNode(): Closure return $this->originalNode; } + #[Override] public function getType(): string { return 'PHPStan_Node_InClosureNode'; @@ -37,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 index b39026a9c9..be8f02f1cc 100644 --- a/src/Node/InForeachNode.php +++ b/src/Node/InForeachNode.php @@ -2,10 +2,11 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Stmt\Foreach_; use PhpParser\NodeAbstract; -class InForeachNode extends NodeAbstract implements VirtualNode +final class InForeachNode extends NodeAbstract implements VirtualNode { public function __construct(private Foreach_ $originalNode) @@ -18,6 +19,7 @@ public function getOriginalNode(): Foreach_ return $this->originalNode; } + #[Override] public function getType(): string { return 'PHPStan_Node_InForeachNode'; @@ -26,6 +28,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/InFunctionNode.php b/src/Node/InFunctionNode.php index 548c2ac422..6393ef0cdd 100644 --- a/src/Node/InFunctionNode.php +++ b/src/Node/InFunctionNode.php @@ -2,11 +2,14 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node; use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; -/** @api */ -class InFunctionNode extends Node\Stmt implements VirtualNode +/** + * @api + */ +final class InFunctionNode extends Node\Stmt implements VirtualNode { public function __construct( @@ -27,6 +30,7 @@ public function getOriginalNode(): Node\Stmt\Function_ return $this->originalNode; } + #[Override] public function getType(): string { return 'PHPStan_Stmt_InFunctionNode'; @@ -35,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 index 688c04499b..bc4f677b9e 100644 --- a/src/Node/InTraitNode.php +++ b/src/Node/InTraitNode.php @@ -2,14 +2,17 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node; use PHPStan\Reflection\ClassReflection; -/** @api */ -class InTraitNode extends Node\Stmt implements VirtualNode +/** + * @api + */ +final class InTraitNode extends Node\Stmt implements VirtualNode { - public function __construct(private Node\Stmt\Trait_ $originalNode, private ClassReflection $traitReflection) + public function __construct(private Node\Stmt\Trait_ $originalNode, private ClassReflection $traitReflection, private ClassReflection $implementingClassReflection) { parent::__construct($originalNode->getAttributes()); } @@ -24,6 +27,12 @@ public function getTraitReflection(): ClassReflection return $this->traitReflection; } + public function getImplementingClassReflection(): ClassReflection + { + return $this->implementingClassReflection; + } + + #[Override] public function getType(): string { return 'PHPStan_Stmt_InTraitNode'; @@ -32,6 +41,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/InstantiationCallableNode.php b/src/Node/InstantiationCallableNode.php index 382431f70a..e7a9f33f6a 100644 --- a/src/Node/InstantiationCallableNode.php +++ b/src/Node/InstantiationCallableNode.php @@ -2,11 +2,14 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Expr; use PhpParser\Node\Name; -/** @api */ -class InstantiationCallableNode extends Expr implements VirtualNode +/** + * @api + */ +final class InstantiationCallableNode extends Expr implements VirtualNode { public function __construct(private Name|Expr $class, private Expr\New_ $originalNode) @@ -27,6 +30,7 @@ public function getOriginalNode(): Expr\New_ return $this->originalNode; } + #[Override] public function getType(): string { return 'PHPStan_Node_InstantiationCallableNode'; @@ -35,6 +39,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/InvalidateExprNode.php b/src/Node/InvalidateExprNode.php index fa8ab6b061..72af3a2f71 100644 --- a/src/Node/InvalidateExprNode.php +++ b/src/Node/InvalidateExprNode.php @@ -2,11 +2,14 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Expr; use PhpParser\NodeAbstract; -/** @api */ -class InvalidateExprNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class InvalidateExprNode extends NodeAbstract implements VirtualNode { public function __construct(private Expr $expr) @@ -19,6 +22,7 @@ public function getExpr(): Expr return $this->expr; } + #[Override] public function getType(): string { return 'PHPStan_Node_InvalidateExpr'; @@ -27,6 +31,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/IssetExpr.php b/src/Node/IssetExpr.php index 5c45df0ebc..9aa4ed2c8b 100644 --- a/src/Node/IssetExpr.php +++ b/src/Node/IssetExpr.php @@ -2,11 +2,18 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Expr; -class IssetExpr extends Expr implements VirtualNode +/** + * @api + */ +final class IssetExpr extends Expr implements VirtualNode { + /** + * @api + */ public function __construct( private Expr $expr, ) @@ -19,6 +26,7 @@ public function getExpr(): Expr return $this->expr; } + #[Override] public function getType(): string { return 'PHPStan_Node_IssetExpr'; @@ -27,6 +35,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/LiteralArrayItem.php b/src/Node/LiteralArrayItem.php index 48dd0e3705..4d9699121b 100644 --- a/src/Node/LiteralArrayItem.php +++ b/src/Node/LiteralArrayItem.php @@ -2,11 +2,13 @@ namespace PHPStan\Node; -use PhpParser\Node\Expr\ArrayItem; +use PhpParser\Node\ArrayItem; use PHPStan\Analyser\Scope; -/** @api */ -class LiteralArrayItem +/** + * @api + */ +final class LiteralArrayItem { public function __construct(private Scope $scope, private ?ArrayItem $arrayItem) diff --git a/src/Node/LiteralArrayNode.php b/src/Node/LiteralArrayNode.php index bcbd3f0a93..3ad49d89d0 100644 --- a/src/Node/LiteralArrayNode.php +++ b/src/Node/LiteralArrayNode.php @@ -2,11 +2,14 @@ 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 { /** @@ -25,6 +28,7 @@ public function getItemNodes(): array return $this->itemNodes; } + #[Override] public function getType(): string { return 'PHPStan_Node_LiteralArray'; @@ -33,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 bc8643af45..bad6265698 100644 --- a/src/Node/MatchExpressionArm.php +++ b/src/Node/MatchExpressionArm.php @@ -2,8 +2,10 @@ namespace PHPStan\Node; -/** @api */ -class MatchExpressionArm +/** + * @api + */ +final class MatchExpressionArm { /** diff --git a/src/Node/MatchExpressionArmBody.php b/src/Node/MatchExpressionArmBody.php index 544d6237de..dbb6f3f917 100644 --- a/src/Node/MatchExpressionArmBody.php +++ b/src/Node/MatchExpressionArmBody.php @@ -5,8 +5,10 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; -/** @api */ -class MatchExpressionArmBody +/** + * @api + */ +final class MatchExpressionArmBody { public function __construct(private Scope $scope, private Expr $body) diff --git a/src/Node/MatchExpressionArmCondition.php b/src/Node/MatchExpressionArmCondition.php index 2928175be7..95a291cce3 100644 --- a/src/Node/MatchExpressionArmCondition.php +++ b/src/Node/MatchExpressionArmCondition.php @@ -5,8 +5,10 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; -/** @api */ -class MatchExpressionArmCondition +/** + * @api + */ +final class MatchExpressionArmCondition { public function __construct(private Expr $condition, private Scope $scope, private int $line) diff --git a/src/Node/MatchExpressionNode.php b/src/Node/MatchExpressionNode.php index 00ec3bcf8c..7cfd531e3e 100644 --- a/src/Node/MatchExpressionNode.php +++ b/src/Node/MatchExpressionNode.php @@ -2,12 +2,15 @@ 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 { /** @@ -41,6 +44,7 @@ public function getEndScope(): Scope return $this->endScope; } + #[Override] public function getType(): string { return 'PHPStan_Node_MatchExpression'; @@ -49,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 19c77316fb..d3726915a9 100644 --- a/src/Node/Method/MethodCall.php +++ b/src/Node/Method/MethodCall.php @@ -7,8 +7,10 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; -/** @api */ -class MethodCall +/** + * @api + */ +final class MethodCall { public function __construct( diff --git a/src/Node/MethodCallableNode.php b/src/Node/MethodCallableNode.php index aff7b5cefc..41aef7a962 100644 --- a/src/Node/MethodCallableNode.php +++ b/src/Node/MethodCallableNode.php @@ -2,11 +2,14 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Expr; use PhpParser\Node\Identifier; -/** @api */ -class MethodCallableNode extends Expr implements VirtualNode +/** + * @api + */ +final class MethodCallableNode extends Expr implements VirtualNode { public function __construct( @@ -36,6 +39,7 @@ public function getOriginalNode(): Expr\MethodCall return $this->originalNode; } + #[Override] public function getType(): string { return 'PHPStan_Node_MethodCallableNode'; @@ -44,6 +48,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/MethodReturnStatementsNode.php b/src/Node/MethodReturnStatementsNode.php index 3151d12ae3..c6b37d3c84 100644 --- a/src/Node/MethodReturnStatementsNode.php +++ b/src/Node/MethodReturnStatementsNode.php @@ -2,6 +2,7 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Expr\Yield_; use PhpParser\Node\Expr\YieldFrom; use PhpParser\Node\Stmt; @@ -10,11 +11,13 @@ use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use function count; -/** @api */ -class MethodReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode +/** + * @api + */ +final class MethodReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode { private ClassMethod $classMethod; @@ -33,7 +36,7 @@ public function __construct( private array $executionEnds, private array $impurePoints, private ClassReflection $classReflection, - private ExtendedMethodReflection $methodReflection, + private PhpMethodFromParserNodeReflection $methodReflection, ) { parent::__construct($method->getAttributes()); @@ -85,7 +88,7 @@ public function getClassReflection(): ClassReflection return $this->classReflection; } - public function getMethodReflection(): ExtendedMethodReflection + public function getMethodReflection(): PhpMethodFromParserNodeReflection { return $this->methodReflection; } @@ -108,6 +111,7 @@ public function isGenerator(): bool return count($this->yieldStatements) > 0; } + #[Override] public function getType(): string { return 'PHPStan_Node_MethodReturnStatementsNode'; @@ -116,6 +120,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/NoopExpressionNode.php b/src/Node/NoopExpressionNode.php index 38e9222a8c..542cf2073e 100644 --- a/src/Node/NoopExpressionNode.php +++ b/src/Node/NoopExpressionNode.php @@ -2,10 +2,11 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Expr; use PhpParser\NodeAbstract; -class NoopExpressionNode extends NodeAbstract implements VirtualNode +final class NoopExpressionNode extends NodeAbstract implements VirtualNode { public function __construct(private Expr $originalExpr, private bool $hasAssign) @@ -23,6 +24,7 @@ public function hasAssign(): bool return $this->hasAssign; } + #[Override] public function getType(): string { return 'PHPStan_Node_NoopExpressionNode'; @@ -31,6 +33,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/Printer/ExprPrinter.php b/src/Node/Printer/ExprPrinter.php index 3763201b5f..7478addf8c 100644 --- a/src/Node/Printer/ExprPrinter.php +++ b/src/Node/Printer/ExprPrinter.php @@ -3,9 +3,13 @@ namespace PHPStan\Node\Printer; use PhpParser\Node\Expr; +use PHPStan\DependencyInjection\AutowiredService; -/** @api */ -class ExprPrinter +/** + * @api + */ +#[AutowiredService] +final class ExprPrinter { public function __construct(private Printer $printer) 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 index 953174fc70..b9579329f3 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -3,6 +3,7 @@ namespace PHPStan\Node\Printer; use PhpParser\PrettyPrinter\Standard; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\AlwaysRememberedExpr; use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\GetIterableKeyTypeExpr; @@ -19,14 +20,13 @@ use PHPStan\Type\VerbosityLevel; use function sprintf; -class Printer extends Standard +/** + * @api + */ +#[AutowiredService(as: Printer::class)] +final class Printer extends Standard { - public function __construct() - { - parent::__construct(['shortArraySyntax' => true]); - } - protected function pPHPStan_Node_TypeExpr(TypeExpr $expr): string // phpcs:ignore { return sprintf('__phpstanType(%s)', $expr->getExprType()->describe(VerbosityLevel::precise())); diff --git a/src/Node/Property/PropertyAssign.php b/src/Node/Property/PropertyAssign.php new file mode 100644 index 0000000000..a88d384a73 --- /dev/null +++ b/src/Node/Property/PropertyAssign.php @@ -0,0 +1,31 @@ +assign; + } + + public function getScope(): Scope + { + return $this->scope; + } + +} diff --git a/src/Node/Property/PropertyRead.php b/src/Node/Property/PropertyRead.php index 27bc3ba5c4..86c220b77f 100644 --- a/src/Node/Property/PropertyRead.php +++ b/src/Node/Property/PropertyRead.php @@ -6,8 +6,10 @@ use PhpParser\Node\Expr\StaticPropertyFetch; use PHPStan\Analyser\Scope; -/** @api */ -class PropertyRead +/** + * @api + */ +final class PropertyRead { public function __construct( diff --git a/src/Node/Property/PropertyWrite.php b/src/Node/Property/PropertyWrite.php index 9577dc7fa8..df39b83d0b 100644 --- a/src/Node/Property/PropertyWrite.php +++ b/src/Node/Property/PropertyWrite.php @@ -6,8 +6,10 @@ use PhpParser\Node\Expr\StaticPropertyFetch; use PHPStan\Analyser\Scope; -/** @api */ -class PropertyWrite +/** + * @api + */ +final class PropertyWrite { public function __construct(private PropertyFetch|StaticPropertyFetch $fetch, private Scope $scope, private bool $promotedPropertyWrite) diff --git a/src/Node/PropertyAssignNode.php b/src/Node/PropertyAssignNode.php index 62a5d7ac1e..7f5b86f03d 100644 --- a/src/Node/PropertyAssignNode.php +++ b/src/Node/PropertyAssignNode.php @@ -2,10 +2,11 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Expr; use PhpParser\NodeAbstract; -class PropertyAssignNode extends NodeAbstract implements VirtualNode +final class PropertyAssignNode extends NodeAbstract implements VirtualNode { public function __construct( @@ -32,6 +33,7 @@ public function isAssignOp(): bool return $this->assignOp; } + #[Override] public function getType(): string { return 'PHPStan_Node_PropertyAssignNodeNode'; @@ -40,6 +42,7 @@ public function getType(): string /** * @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 99767fba7b..153faf1534 100644 --- a/src/Node/ReturnStatement.php +++ b/src/Node/ReturnStatement.php @@ -6,8 +6,10 @@ use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; -/** @api */ -class ReturnStatement +/** + * @api + */ +final class ReturnStatement { private Node\Stmt\Return_ $returnNode; diff --git a/src/Node/StaticMethodCallableNode.php b/src/Node/StaticMethodCallableNode.php index a4c6e675ef..ece87a2719 100644 --- a/src/Node/StaticMethodCallableNode.php +++ b/src/Node/StaticMethodCallableNode.php @@ -2,12 +2,15 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Expr; use PhpParser\Node\Identifier; use PhpParser\Node\Name; -/** @api */ -class StaticMethodCallableNode extends Expr implements VirtualNode +/** + * @api + */ +final class StaticMethodCallableNode extends Expr implements VirtualNode { public function __construct( @@ -40,6 +43,7 @@ public function getOriginalNode(): Expr\StaticCall return $this->originalNode; } + #[Override] public function getType(): string { return 'PHPStan_Node_StaticMethodCallableNode'; @@ -48,6 +52,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/UnreachableStatementNode.php b/src/Node/UnreachableStatementNode.php index fb05c30a50..60991bb115 100644 --- a/src/Node/UnreachableStatementNode.php +++ b/src/Node/UnreachableStatementNode.php @@ -2,13 +2,17 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Stmt; -/** @api */ -class UnreachableStatementNode extends Stmt implements VirtualNode +/** + * @api + */ +final class UnreachableStatementNode extends Stmt implements VirtualNode { - public function __construct(private Stmt $originalStatement) + /** @param Stmt[] $nextStatements */ + public function __construct(private Stmt $originalStatement, private array $nextStatements = []) { parent::__construct($originalStatement->getAttributes()); } @@ -18,6 +22,7 @@ public function getOriginalStatement(): Stmt return $this->originalStatement; } + #[Override] public function getType(): string { return 'PHPStan_Stmt_UnreachableStatementNode'; @@ -26,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 index 6689aa7a0d..cfd3e9c742 100644 --- a/src/Node/VarTagChangedExpressionTypeNode.php +++ b/src/Node/VarTagChangedExpressionTypeNode.php @@ -2,11 +2,12 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Expr; use PhpParser\NodeAbstract; use PHPStan\PhpDoc\Tag\VarTag; -class VarTagChangedExpressionTypeNode extends NodeAbstract implements VirtualNode +final class VarTagChangedExpressionTypeNode extends NodeAbstract implements VirtualNode { public function __construct(private VarTag $varTag, private Expr $expr) @@ -24,6 +25,7 @@ public function getExpr(): Expr return $this->expr; } + #[Override] public function getType(): string { return 'PHPStan_Node_VarTagChangedExpressionType'; @@ -32,6 +34,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Node/VariableAssignNode.php b/src/Node/VariableAssignNode.php index 2857e853ba..678d5bb348 100644 --- a/src/Node/VariableAssignNode.php +++ b/src/Node/VariableAssignNode.php @@ -2,16 +2,16 @@ namespace PHPStan\Node; +use Override; use PhpParser\Node\Expr; use PhpParser\NodeAbstract; -class VariableAssignNode extends NodeAbstract implements VirtualNode +final class VariableAssignNode extends NodeAbstract implements VirtualNode { public function __construct( private Expr\Variable $variable, private Expr $assignedExpr, - private bool $assignOp, ) { parent::__construct($variable->getAttributes()); @@ -27,11 +27,7 @@ public function getAssignedExpr(): Expr return $this->assignedExpr; } - public function isAssignOp(): bool - { - return $this->assignOp; - } - + #[Override] public function getType(): string { return 'PHPStan_Node_VariableAssignNodeNode'; @@ -40,6 +36,7 @@ public function getType(): string /** * @return string[] */ + #[Override] public function getSubNodeNames(): array { return []; diff --git a/src/Parallel/ParallelAnalyser.php b/src/Parallel/ParallelAnalyser.php index 65f8776c19..8503d826e0 100644 --- a/src/Parallel/ParallelAnalyser.php +++ b/src/Parallel/ParallelAnalyser.php @@ -9,8 +9,9 @@ use PHPStan\Analyser\AnalyserResult; use PHPStan\Analyser\Error; use PHPStan\Analyser\InternalError; -use PHPStan\Collectors\CollectedData; use PHPStan\Dependency\RootExportedNode; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Process\ProcessHelper; use React\EventLoop\LoopInterface; use React\Promise\Deferred; @@ -25,6 +26,7 @@ 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; @@ -33,7 +35,8 @@ use function str_contains; use const PHP_URL_PORT; -class ParallelAnalyser +#[AutowiredService] +final class ParallelAnalyser { private const DEFAULT_TIMEOUT = 600.0; @@ -43,8 +46,11 @@ class ParallelAnalyser private ProcessPool $processPool; public function __construct( + #[AutowiredParameter] private int $internalErrorsCountLimit, + #[AutowiredParameter(ref: '%parallel.processTimeout%')] float $processTimeout, + #[AutowiredParameter(ref: '%parallel.buffer%')] private int $decoderBufferSize, ) { @@ -62,6 +68,8 @@ public function analyse( string $mainScript, ?Closure $postFileCallback, ?string $projectConfigFile, + ?string $tmpFile, + ?string $insteadOfFile, InputInterface $input, ?callable $onFileAnalysisHandler, ): PromiseInterface @@ -81,6 +89,7 @@ public function analyse( $internalErrorsCount = 0; $collectedData = []; $dependencies = []; + $usedTraitDependencies = []; $reachedInternalErrorsCountLimit = false; $exportedNodes = []; @@ -88,7 +97,7 @@ public function analyse( $deferred = new Deferred(); $server = new TcpServer('127.0.0.1:0', $loop); - $this->processPool = new ProcessPool($server, static function () use ($deferred, &$jobs, &$internalErrors, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, &$errors, &$filteredPhpErrors, &$allPhpErrors, &$locallyIgnoredErrors, &$linesToIgnore, &$unmatchedLineIgnores, &$collectedData, &$dependencies, &$exportedNodes, &$peakMemoryUsages): void { + $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.', @@ -110,6 +119,7 @@ public function analyse( $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 @@ -119,7 +129,7 @@ public function analyse( // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; // phpcs:enable - $decoder = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, $this->decoderBufferSize); + $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') { @@ -170,6 +180,13 @@ public function analyse( $processIdentifier, ]; + if ($tmpFile !== null && $insteadOfFile !== null) { + $commandOptions[] = '--tmp-file'; + $commandOptions[] = escapeshellarg($tmpFile); + $commandOptions[] = '--instead-of'; + $commandOptions[] = escapeshellarg($insteadOfFile); + } + $process = new Process(ProcessHelper::getWorkerCommand( $mainScript, 'worker', @@ -177,7 +194,7 @@ public function analyse( $commandOptions, $input, ), $loop, $this->processTimeout); - $process->start(function (array $json) use ($process, &$internalErrors, &$errors, &$filteredPhpErrors, &$allPhpErrors, &$locallyIgnoredErrors, &$linesToIgnore, &$unmatchedLineIgnores, &$collectedData, &$dependencies, &$exportedNodes, &$peakMemoryUsages, &$jobs, $postFileCallback, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, $processIdentifier, $onFileAnalysisHandler): void { + $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) { $fileErrors[] = Error::decode($jsonError); @@ -211,8 +228,12 @@ public function analyse( $locallyIgnoredErrors[] = $locallyIgnoredFileError; } - foreach ($json['collectedData'] as $jsonData) { - $collectedData[] = CollectedData::decode($jsonData); + foreach ($json['collectedData'] as $file => $jsonDataByCollector) { + foreach ($jsonDataByCollector as $collectorType => $listOfCollectedData) { + foreach ($listOfCollectedData as $rawCollectedData) { + $collectedData[$file][$collectorType][] = $rawCollectedData; + } + } } /** @@ -223,6 +244,14 @@ 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; @@ -279,11 +308,12 @@ public function analyse( } $someChildEnded = true; - $this->processPool->tryQuitProcess($processIdentifier); if ($exitCode === 0) { + $this->processPool->tryQuitProcess($processIdentifier); return; } if ($exitCode === null) { + $this->processPool->tryQuitProcess($processIdentifier); return; } @@ -294,6 +324,7 @@ public function analyse( continue; } + $this->processPool->tryQuitProcess($processIdentifier); return; } $internalErrors[] = new InternalError(sprintf( @@ -303,11 +334,13 @@ public function analyse( 'Increase your memory limit in php.ini or run PHPStan with --memory-limit CLI option.', ), 'running parallel worker', [], null, false); $internalErrorsCount++; + $this->processPool->tryQuitProcess($processIdentifier); return; } $internalErrors[] = new InternalError(sprintf('Child process error (exit code %d): %s', $exitCode, $output), 'running parallel worker', [], null, true); $internalErrorsCount++; + $this->processPool->tryQuitProcess($processIdentifier); }); $this->processPool->attachProcess($processIdentifier, $process); } diff --git a/src/Parallel/Process.php b/src/Parallel/Process.php index d95609b6ef..36799c274d 100644 --- a/src/Parallel/Process.php +++ b/src/Parallel/Process.php @@ -9,13 +9,12 @@ use React\Stream\WritableStreamInterface; use Throwable; use function fclose; -use function is_string; use function rewind; use function sprintf; use function stream_get_contents; use function tmpfile; -class Process +final class Process { public \React\ChildProcess\Process $process; @@ -61,7 +60,7 @@ public function start(callable $onData, callable $onError, callable $onExit): vo } $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, ]); @@ -73,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); diff --git a/src/Parallel/ProcessPool.php b/src/Parallel/ProcessPool.php index ac0569c509..574cabcf6e 100644 --- a/src/Parallel/ProcessPool.php +++ b/src/Parallel/ProcessPool.php @@ -9,7 +9,7 @@ use function count; use function sprintf; -class ProcessPool +final class ProcessPool { /** @var array */ diff --git a/src/Parallel/ProcessTimedOutException.php b/src/Parallel/ProcessTimedOutException.php index 77e42107f7..50667999a1 100644 --- a/src/Parallel/ProcessTimedOutException.php +++ b/src/Parallel/ProcessTimedOutException.php @@ -4,7 +4,7 @@ use Exception; -class ProcessTimedOutException extends Exception +final class ProcessTimedOutException extends Exception { } diff --git a/src/Parallel/Schedule.php b/src/Parallel/Schedule.php index 42935bd6c5..b37935a4ce 100644 --- a/src/Parallel/Schedule.php +++ b/src/Parallel/Schedule.php @@ -2,7 +2,7 @@ namespace PHPStan\Parallel; -class Schedule +final class Schedule { /** diff --git a/src/Parallel/Scheduler.php b/src/Parallel/Scheduler.php index 77394959b2..239db76681 100644 --- a/src/Parallel/Scheduler.php +++ b/src/Parallel/Scheduler.php @@ -3,6 +3,8 @@ namespace PHPStan\Parallel; use PHPStan\Command\Output; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Diagnose\DiagnoseExtension; use function array_chunk; use function count; @@ -11,7 +13,8 @@ use function min; use function sprintf; -class Scheduler implements DiagnoseExtension +#[AutowiredService] +final class Scheduler implements DiagnoseExtension { /** @var array{int, int, int, int}|null */ @@ -23,8 +26,11 @@ class Scheduler implements DiagnoseExtension * @param positive-int $minimumNumberOfJobsPerProcess */ public function __construct( + #[AutowiredParameter(ref: '%parallel.jobSize%')] private int $jobSize, + #[AutowiredParameter(ref: '%parallel.maximumNumberOfProcesses%')] private int $maximumNumberOfProcesses, + #[AutowiredParameter(ref: '%parallel.minimumNumberOfJobsPerProcess%')] private int $minimumNumberOfJobsPerProcess, ) { diff --git a/src/Parser/AnonymousClassVisitor.php b/src/Parser/AnonymousClassVisitor.php index 33ca16c567..77e9840633 100644 --- a/src/Parser/AnonymousClassVisitor.php +++ b/src/Parser/AnonymousClassVisitor.php @@ -2,12 +2,15 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\AnonymousClassNode; use function count; -class AnonymousClassVisitor extends NodeVisitorAbstract +#[AutowiredService] +final class AnonymousClassVisitor extends NodeVisitorAbstract { public const ATTRIBUTE_LINE_INDEX = 'anonymousClassLineIndex'; @@ -15,12 +18,14 @@ class AnonymousClassVisitor extends NodeVisitorAbstract /** @var array> */ 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()) { @@ -34,6 +39,7 @@ public function enterNode(Node $node): ?Node return $node; } + #[Override] public function afterTraverse(array $nodes): ?array { foreach ($this->nodesPerLine as $nodesOnLine) { diff --git a/src/Parser/ArrayFilterArgVisitor.php b/src/Parser/ArrayFilterArgVisitor.php index bf7f9eef75..af5c2e41fb 100644 --- a/src/Parser/ArrayFilterArgVisitor.php +++ b/src/Parser/ArrayFilterArgVisitor.php @@ -2,14 +2,18 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\DependencyInjection\AutowiredService; -class ArrayFilterArgVisitor extends NodeVisitorAbstract +#[AutowiredService] +final class ArrayFilterArgVisitor extends NodeVisitorAbstract { public const ATTRIBUTE_NAME = 'isArrayFilterArg'; + #[Override] public function enterNode(Node $node): ?Node { if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { 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 index b6e7a12ccb..c9e5b8a358 100644 --- a/src/Parser/ArrayMapArgVisitor.php +++ b/src/Parser/ArrayMapArgVisitor.php @@ -2,31 +2,75 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; -use function array_slice; +use PHPStan\DependencyInjection\AutowiredService; use function count; -class ArrayMapArgVisitor extends NodeVisitorAbstract +#[AutowiredService] +final class ArrayMapArgVisitor extends NodeVisitorAbstract { public const ATTRIBUTE_NAME = 'arrayMapArgs'; + #[Override] public function enterNode(Node $node): ?Node { - if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { - $functionName = $node->name->toLowerString(); - if ($functionName === 'array_map') { - $args = $node->getArgs(); - if (isset($args[0])) { - $slicedArgs = array_slice($args, 1); - if (count($slicedArgs) > 0) { - $args[0]->value->setAttribute(self::ATTRIBUTE_NAME, $slicedArgs); - } + if (!$this->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 index addc7dc1ba..f4a0a05022 100644 --- a/src/Parser/ArrayWalkArgVisitor.php +++ b/src/Parser/ArrayWalkArgVisitor.php @@ -2,14 +2,18 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\DependencyInjection\AutowiredService; -class ArrayWalkArgVisitor extends NodeVisitorAbstract +#[AutowiredService] +final class ArrayWalkArgVisitor extends NodeVisitorAbstract { public const ATTRIBUTE_NAME = 'isArrayWalkArg'; + #[Override] public function enterNode(Node $node): ?Node { if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { diff --git a/src/Parser/ArrowFunctionArgVisitor.php b/src/Parser/ArrowFunctionArgVisitor.php index 24b04718fa..948f48625d 100644 --- a/src/Parser/ArrowFunctionArgVisitor.php +++ b/src/Parser/ArrowFunctionArgVisitor.php @@ -2,24 +2,43 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\DependencyInjection\AutowiredService; use function count; -class ArrowFunctionArgVisitor extends NodeVisitorAbstract +#[AutowiredService] +final class ArrowFunctionArgVisitor extends NodeVisitorAbstract { public const ATTRIBUTE_NAME = 'arrowFunctionCallArgs'; + #[Override] public function enterNode(Node $node): ?Node { - if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Expr\ArrowFunction && !$node->isFirstClassCallable()) { - $args = $node->getArgs(); + if (!$node instanceof Node\Expr\FuncCall) { + return null; + } + + if ($node->isFirstClassCallable()) { + return null; + } - if (count($args) > 0) { - $node->name->setAttribute(self::ATTRIBUTE_NAME, $args); - } + if ($node->name instanceof Node\Expr\Assign && $node->name->expr instanceof Node\Expr\ArrowFunction) { + $arrow = $node->name->expr; + } elseif ($node->name instanceof Node\Expr\ArrowFunction) { + $arrow = $node->name; + } else { + return null; } + + $args = $node->getArgs(); + + if (count($args) > 0) { + $arrow->setAttribute(self::ATTRIBUTE_NAME, $args); + } + return null; } diff --git a/src/Parser/CachedParser.php b/src/Parser/CachedParser.php index 225375678c..400c21bf5a 100644 --- a/src/Parser/CachedParser.php +++ b/src/Parser/CachedParser.php @@ -6,7 +6,7 @@ use PHPStan\File\FileReader; use function array_slice; -class CachedParser implements Parser +final class CachedParser implements Parser { /** @var array*/ @@ -34,8 +34,7 @@ public function parseFile(string $file): array $this->cachedNodesByString = array_slice( $this->cachedNodesByString, 1, - null, - true, + preserve_keys: true, ); --$this->cachedNodesByStringCount; @@ -60,8 +59,7 @@ public function parseString(string $sourceCode): array $this->cachedNodesByString = array_slice( $this->cachedNodesByString, 1, - null, - true, + preserve_keys: true, ); --$this->cachedNodesByStringCount; diff --git a/src/Parser/CleaningParser.php b/src/Parser/CleaningParser.php index 98db0e64ef..0f874eafbf 100644 --- a/src/Parser/CleaningParser.php +++ b/src/Parser/CleaningParser.php @@ -6,7 +6,7 @@ use PhpParser\NodeTraverser; use PHPStan\Php\PhpVersion; -class CleaningParser implements Parser +final class CleaningParser implements Parser { private NodeTraverser $traverser; diff --git a/src/Parser/CleaningVisitor.php b/src/Parser/CleaningVisitor.php index 0a2c9aecff..c0f4c0b458 100644 --- a/src/Parser/CleaningVisitor.php +++ b/src/Parser/CleaningVisitor.php @@ -2,13 +2,15 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\NodeFinder; use PhpParser\NodeVisitorAbstract; use PHPStan\Reflection\ParametersAcceptor; use function in_array; +use function is_array; -class CleaningVisitor extends NodeVisitorAbstract +final class CleaningVisitor extends NodeVisitorAbstract { private NodeFinder $nodeFinder; @@ -18,23 +20,32 @@ public function __construct() $this->nodeFinder = new NodeFinder(); } + #[Override] public function enterNode(Node $node): ?Node { if ($node instanceof Node\Stmt\Function_) { - $node->stmts = $this->keepVariadicsAndYields($node->stmts); + $node->stmts = $this->keepVariadicsAndYields($node->stmts, null); return $node; } if ($node instanceof Node\Stmt\ClassMethod && $node->stmts !== null) { - $node->stmts = $this->keepVariadicsAndYields($node->stmts); + $node->stmts = $this->keepVariadicsAndYields($node->stmts, null); return $node; } if ($node instanceof Node\Expr\Closure) { - $node->stmts = $this->keepVariadicsAndYields($node->stmts); + $node->stmts = $this->keepVariadicsAndYields($node->stmts, null); return $node; } + if ($node instanceof Node\PropertyHook && is_array($node->body)) { + $propertyName = $node->getAttribute('propertyName'); + if ($propertyName !== null) { + $node->body = $this->keepVariadicsAndYields($node->body, $propertyName); + return $node; + } + } + return null; } @@ -42,9 +53,9 @@ public function enterNode(Node $node): ?Node * @param Node\Stmt[] $stmts * @return Node\Stmt[] */ - private function keepVariadicsAndYields(array $stmts): array + private function keepVariadicsAndYields(array $stmts, ?string $hookedPropertyName): array { - $results = $this->nodeFinder->find($stmts, static function (Node $node): bool { + $results = $this->nodeFinder->find($stmts, static function (Node $node) use ($hookedPropertyName): bool { if ($node instanceof Node\Expr\YieldFrom || $node instanceof Node\Expr\Yield_) { return true; } @@ -56,6 +67,18 @@ private function keepVariadicsAndYields(array $stmts): array 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 = []; @@ -65,6 +88,7 @@ private function keepVariadicsAndYields(array $stmts): array || $result instanceof Node\Expr\YieldFrom || $result instanceof Node\Expr\Closure || $result instanceof Node\Expr\ArrowFunction + || $result instanceof Node\Expr\PropertyFetch ) { $newStmts[] = new Node\Stmt\Expression($result); continue; diff --git a/src/Parser/ClosureArgVisitor.php b/src/Parser/ClosureArgVisitor.php index df36796570..30533190b5 100644 --- a/src/Parser/ClosureArgVisitor.php +++ b/src/Parser/ClosureArgVisitor.php @@ -2,24 +2,43 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\DependencyInjection\AutowiredService; use function count; -class ClosureArgVisitor extends NodeVisitorAbstract +#[AutowiredService] +final class ClosureArgVisitor extends NodeVisitorAbstract { public const ATTRIBUTE_NAME = 'closureCallArgs'; + #[Override] public function enterNode(Node $node): ?Node { - if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Expr\Closure && !$node->isFirstClassCallable()) { - $args = $node->getArgs(); + if (!$node instanceof Node\Expr\FuncCall) { + return null; + } + + if ($node->isFirstClassCallable()) { + return null; + } - if (count($args) > 0) { - $node->name->setAttribute(self::ATTRIBUTE_NAME, $args); - } + 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 index de341a57d9..ed94333cad 100644 --- a/src/Parser/ClosureBindArgVisitor.php +++ b/src/Parser/ClosureBindArgVisitor.php @@ -2,16 +2,20 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\Node\Identifier; use PhpParser\NodeVisitorAbstract; +use PHPStan\DependencyInjection\AutowiredService; use function count; -class ClosureBindArgVisitor extends NodeVisitorAbstract +#[AutowiredService] +final class ClosureBindArgVisitor extends NodeVisitorAbstract { public const ATTRIBUTE_NAME = 'closureBindArg'; + #[Override] public function enterNode(Node $node): ?Node { if ( diff --git a/src/Parser/ClosureBindToVarVisitor.php b/src/Parser/ClosureBindToVarVisitor.php index f3582ba617..a94de9f6ef 100644 --- a/src/Parser/ClosureBindToVarVisitor.php +++ b/src/Parser/ClosureBindToVarVisitor.php @@ -2,15 +2,19 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\Node\Identifier; use PhpParser\NodeVisitorAbstract; +use PHPStan\DependencyInjection\AutowiredService; -class ClosureBindToVarVisitor extends NodeVisitorAbstract +#[AutowiredService] +final class ClosureBindToVarVisitor extends NodeVisitorAbstract { public const ATTRIBUTE_NAME = 'closureBindToVar'; + #[Override] public function enterNode(Node $node): ?Node { if ( diff --git a/src/Parser/CurlSetOptArgVisitor.php b/src/Parser/CurlSetOptArgVisitor.php index 13910ec5a1..16111b34fd 100644 --- a/src/Parser/CurlSetOptArgVisitor.php +++ b/src/Parser/CurlSetOptArgVisitor.php @@ -2,14 +2,18 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\DependencyInjection\AutowiredService; -class CurlSetOptArgVisitor extends NodeVisitorAbstract +#[AutowiredService] +final class CurlSetOptArgVisitor extends NodeVisitorAbstract { public const ATTRIBUTE_NAME = 'isCurlSetOptArg'; + #[Override] public function enterNode(Node $node): ?Node { if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { diff --git a/src/Parser/DeclarePositionVisitor.php b/src/Parser/DeclarePositionVisitor.php index 08818c1652..11f8ebb00e 100644 --- a/src/Parser/DeclarePositionVisitor.php +++ b/src/Parser/DeclarePositionVisitor.php @@ -2,23 +2,28 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\DependencyInjection\AutowiredService; use function str_starts_with; -class DeclarePositionVisitor extends NodeVisitorAbstract +#[AutowiredService] +final class DeclarePositionVisitor extends NodeVisitorAbstract { private bool $isFirstStatement = true; public const ATTRIBUTE_NAME = 'isFirstStatement'; + #[Override] public function beforeTraverse(array $nodes): ?array { $this->isFirstStatement = true; return null; } + #[Override] public function enterNode(Node $node): ?Node { // ignore shebang diff --git a/src/Parser/FunctionCallStatementFinder.php b/src/Parser/FunctionCallStatementFinder.php deleted file mode 100644 index 4f1b190d35..0000000000 --- a/src/Parser/FunctionCallStatementFinder.php +++ /dev/null @@ -1,47 +0,0 @@ -findFunctionCallInStatements($functionNames, $statement); - if ($result !== null) { - return $result; - } - } - - if (!($statement instanceof Node)) { - continue; - } - - if ($statement instanceof FuncCall && $statement->name instanceof Name) { - if (in_array((string) $statement->name, $functionNames, true)) { - return $statement; - } - } - - $result = $this->findFunctionCallInStatements($functionNames, $statement); - if ($result !== null) { - return $result; - } - } - - return null; - } - -} diff --git a/src/Parser/ImmediatelyInvokedClosureVisitor.php b/src/Parser/ImmediatelyInvokedClosureVisitor.php new file mode 100644 index 0000000000..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/LastConditionVisitor.php b/src/Parser/LastConditionVisitor.php index 5edd559ed0..f122206e0b 100644 --- a/src/Parser/LastConditionVisitor.php +++ b/src/Parser/LastConditionVisitor.php @@ -2,23 +2,31 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\DependencyInjection\AutowiredService; use function count; -class LastConditionVisitor extends NodeVisitorAbstract +#[AutowiredService] +final class LastConditionVisitor extends NodeVisitorAbstract { public const ATTRIBUTE_NAME = 'isLastCondition'; public const ATTRIBUTE_IS_MATCH_NAME = 'isMatch'; + #[Override] public function enterNode(Node $node): ?Node { if ($node instanceof Node\Stmt\If_ && $node->elseifs !== []) { $lastElseIf = count($node->elseifs) - 1; $elseIsMissingOrThrowing = $node->else === null - || (count($node->else->stmts) === 1 && $node->else->stmts[0] instanceof Node\Stmt\Throw_); + || ( + 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; @@ -64,7 +72,13 @@ public function enterNode(Node $node): ?Node return null; } - if (!$statements[$statementCount - 1] instanceof Node\Stmt\Throw_) { + $lastStatement = $statements[$statementCount - 1]; + + if (!$lastStatement instanceof Node\Stmt\Expression) { + return null; + } + + if (!$lastStatement->expr instanceof Node\Expr\Throw_) { return null; } diff --git a/src/Parser/LexerFactory.php b/src/Parser/LexerFactory.php index 624eb251bf..1f844ff7cd 100644 --- a/src/Parser/LexerFactory.php +++ b/src/Parser/LexerFactory.php @@ -3,33 +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 const OPTIONS = ['usedAttributes' => ['comments', 'startLine', 'endLine', 'startTokenPos', 'endTokenPos', 'startFilePos', 'endFilePos']]; - public function __construct(private PhpVersion $phpVersion) { } public function create(): Lexer { - $options = self::OPTIONS; if ($this->phpVersion->getVersionId() === PHP_VERSION_ID) { - return new Lexer($options); + return new Lexer(); } - $options['phpVersion'] = $this->phpVersion->getVersionString(); - - return new Lexer\Emulative($options); + return new Lexer\Emulative(\PhpParser\PhpVersion::fromString($this->phpVersion->getVersionString())); } public function createEmulative(): Lexer\Emulative { - return new Lexer\Emulative(self::OPTIONS); + 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 index 5bc27ada08..5966f7f8e6 100644 --- a/src/Parser/MagicConstantParamDefaultVisitor.php +++ b/src/Parser/MagicConstantParamDefaultVisitor.php @@ -2,14 +2,18 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\DependencyInjection\AutowiredService; -class MagicConstantParamDefaultVisitor extends NodeVisitorAbstract +#[AutowiredService] +final class MagicConstantParamDefaultVisitor extends NodeVisitorAbstract { public const ATTRIBUTE_NAME = 'isMagicConstantParamDefault'; + #[Override] public function enterNode(Node $node): ?Node { if ($node instanceof Node\Param && $node->default instanceof Node\Scalar\MagicConst) { diff --git a/src/Parser/NewAssignedToPropertyVisitor.php b/src/Parser/NewAssignedToPropertyVisitor.php index a38bab16fb..60fd8fd72e 100644 --- a/src/Parser/NewAssignedToPropertyVisitor.php +++ b/src/Parser/NewAssignedToPropertyVisitor.php @@ -2,18 +2,25 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\DependencyInjection\AutowiredService; -class NewAssignedToPropertyVisitor extends NodeVisitorAbstract +#[AutowiredService] +final class NewAssignedToPropertyVisitor extends NodeVisitorAbstract { public const ATTRIBUTE_NAME = 'assignedToProperty'; + #[Override] public function enterNode(Node $node): ?Node { if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignRef) { - if ($node->var instanceof Node\Expr\PropertyFetch && $node->expr instanceof Node\Expr\New_) { + if ( + ($node->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); } } diff --git a/src/Parser/ParentStmtTypesVisitor.php b/src/Parser/ParentStmtTypesVisitor.php index a7da560658..faaa4024ee 100644 --- a/src/Parser/ParentStmtTypesVisitor.php +++ b/src/Parser/ParentStmtTypesVisitor.php @@ -2,12 +2,15 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\DependencyInjection\AutowiredService; use function array_pop; use function count; use function get_class; +#[AutowiredService] final class ParentStmtTypesVisitor extends NodeVisitorAbstract { @@ -16,12 +19,14 @@ final class ParentStmtTypesVisitor extends NodeVisitorAbstract /** @var array> */ 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) { @@ -36,6 +41,7 @@ public function enterNode(Node $node): ?Node return null; } + #[Override] public function leaveNode(Node $node): ?Node { if (!$node instanceof Node\Stmt && !$node instanceof Node\Expr\Closure) { diff --git a/src/Parser/ParserErrorsException.php b/src/Parser/ParserErrorsException.php index 1013be73d4..d68d18220a 100644 --- a/src/Parser/ParserErrorsException.php +++ b/src/Parser/ParserErrorsException.php @@ -8,7 +8,7 @@ use function count; use function implode; -class ParserErrorsException extends Exception +final class ParserErrorsException extends Exception { /** @var mixed[] */ diff --git a/src/Parser/PathRoutingParser.php b/src/Parser/PathRoutingParser.php index def6786f5e..0eb55ab07d 100644 --- a/src/Parser/PathRoutingParser.php +++ b/src/Parser/PathRoutingParser.php @@ -13,9 +13,11 @@ use function str_contains; use const DIRECTORY_SEPARATOR; -class PathRoutingParser implements Parser +final class PathRoutingParser implements Parser { + private ?string $singleReflectionFile; + /** @var bool[] filePath(string) => bool(true) */ private array $analysedFiles = []; @@ -24,8 +26,10 @@ public function __construct( private Parser $currentPhpVersionRichParser, private Parser $currentPhpVersionSimpleParser, private Parser $php8Parser, + ?string $singleReflectionFile, ) { + $this->singleReflectionFile = $singleReflectionFile !== null ? $fileHelper->normalizePath($singleReflectionFile) : null; } /** @@ -47,7 +51,7 @@ public function parseFile(string $file): array } $file = $this->fileHelper->normalizePath($file); - if (!isset($this->analysedFiles[$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--) { diff --git a/src/Parser/PhpParserDecorator.php b/src/Parser/PhpParserDecorator.php index 5626f20b1a..a8a2102f33 100644 --- a/src/Parser/PhpParserDecorator.php +++ b/src/Parser/PhpParserDecorator.php @@ -2,13 +2,15 @@ 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 Parser +final class PhpParserDecorator implements Parser { public function __construct(private \PHPStan\Parser\Parser $wrappedParser) @@ -18,6 +20,7 @@ public function __construct(private \PHPStan\Parser\Parser $wrappedParser) /** * @return Node\Stmt[] */ + #[Override] public function parse(string $code, ?ErrorHandler $errorHandler = null): array { try { @@ -31,4 +34,10 @@ public function parse(string $code, ?ErrorHandler $errorHandler = null): array } } + #[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 index eed9b93bf7..6807d9ccaa 100644 --- a/src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php +++ b/src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php @@ -2,20 +2,22 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; use PHPStan\Php\PhpVersion; use function count; use function version_compare; -class RemoveUnusedCodeByPhpVersionIdVisitor extends NodeVisitorAbstract +final class RemoveUnusedCodeByPhpVersionIdVisitor extends NodeVisitorAbstract { public function __construct(private string $phpVersionString) { } - public function enterNode(Node $node): Node|int|null + #[Override] + public function enterNode(Node $node): ?Node { if (!$node instanceof Node\Stmt\If_) { return null; @@ -77,7 +79,7 @@ public function enterNode(Node $node): Node|int|null private function getOperands(Node\Expr $left, Node\Expr $right): ?array { if ( - $left instanceof Node\Scalar\LNumber + $left instanceof Node\Scalar\Int_ && $right instanceof Node\Expr\ConstFetch && $right->name->toString() === 'PHP_VERSION_ID' ) { @@ -85,7 +87,7 @@ private function getOperands(Node\Expr $left, Node\Expr $right): ?array } if ( - $right instanceof Node\Scalar\LNumber + $right instanceof Node\Scalar\Int_ && $left instanceof Node\Expr\ConstFetch && $left->name->toString() === 'PHP_VERSION_ID' ) { diff --git a/src/Parser/RichParser.php b/src/Parser/RichParser.php index 11281ca7fd..a50ec45dbe 100644 --- a/src/Parser/RichParser.php +++ b/src/Parser/RichParser.php @@ -3,10 +3,10 @@ namespace PHPStan\Parser; use PhpParser\ErrorHandler\Collecting; -use PhpParser\Lexer; use PhpParser\Node; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; +use PhpParser\Token; use PHPStan\Analyser\Ignore\IgnoreLexer; use PHPStan\Analyser\Ignore\IgnoreParseException; use PHPStan\DependencyInjection\Container; @@ -17,7 +17,6 @@ use function count; use function implode; use function in_array; -use function is_string; use function preg_match_all; use function sprintf; use function str_contains; @@ -31,7 +30,7 @@ use const T_DOC_COMMENT; use const T_WHITESPACE; -class RichParser implements Parser +final class RichParser implements Parser { public const VISITOR_SERVICE_TAG = 'phpstan.parser.richParserNodeVisitor'; @@ -42,11 +41,9 @@ class RichParser implements Parser public function __construct( private \PhpParser\Parser $parser, - private Lexer $lexer, private NameResolver $nameResolver, private Container $container, private IgnoreLexer $ignoreLexer, - private bool $enableIgnoreErrorsWithinPhpDocs = false, ) { } @@ -72,8 +69,7 @@ public function parseString(string $sourceCode): array $errorHandler = new Collecting(); $nodes = $this->parser->parse($sourceCode, $errorHandler); - /** @var list $tokens */ - $tokens = $this->lexer->getTokens(); + $tokens = $this->parser->getTokens(); if ($errorHandler->hasErrors()) { throw new ParserErrorsException($errorHandler->getErrors(), null); } @@ -102,14 +98,19 @@ public function parseString(string $sourceCode): array } foreach ($traitCollectingVisitor->traits as $trait) { - $trait->setAttribute('linesToIgnore', array_filter($linesToIgnore, static fn (int $line): bool => $line >= $trait->getStartLine() && $line <= $trait->getEndLine(), ARRAY_FILTER_USE_KEY)); + $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 list $tokens + * @param Token[] $tokens * @return array{lines: array|null>, errors: array>} */ private function getLinesToIgnore(array $tokens): array @@ -119,12 +120,8 @@ private function getLinesToIgnore(array $tokens): array $pendingToken = null; $errors = []; foreach ($tokens as $token) { - if (is_string($token)) { - continue; - } - - $type = $token[0]; - $line = $token[2]; + $type = $token->id; + $line = $token->line; if ($type !== T_COMMENT && $type !== T_DOC_COMMENT) { if ($type !== T_WHITESPACE) { if ($pendingToken !== null) { @@ -155,12 +152,12 @@ private function getLinesToIgnore(array $tokens): array continue; } - $text = $token[1]; + $text = $token->text; $isNextLine = str_contains($text, '@phpstan-ignore-next-line'); $isCurrentLine = str_contains($text, '@phpstan-ignore-line'); - if ($this->enableIgnoreErrorsWithinPhpDocs && $type === T_DOC_COMMENT) { - $lines += $this->getLinesToIgnoreForTokenByIgnoreComment($text, $line, '@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); @@ -190,7 +187,7 @@ private function getLinesToIgnore(array $tokens): array $line++; } if ($isNextLine || $isCurrentLine) { - $line += substr_count($token[1], "\n"); + $line += substr_count($token->text, "\n"); $lines[$line] = null; continue; @@ -204,20 +201,20 @@ private function getLinesToIgnore(array $tokens): array $ignoreLine = substr_count(substr($text, 0, $ignorePos), "\n") - 1; - if ($previousToken !== null && $previousToken[2] === $line) { + if ($previousToken !== null && $previousToken->line === $line) { try { foreach ($this->parseIdentifiers($text, $ignorePos) as $identifier) { $lines[$line][] = $identifier; } } catch (IgnoreParseException $e) { - $errors[] = [$token[2] + $e->getPhpDocLine() + $ignoreLine, $e->getMessage()]; + $errors[] = [$token->line + $e->getPhpDocLine() + $ignoreLine, $e->getMessage()]; } continue; } - $line += substr_count($token[1], "\n"); - $pendingToken = [$text, $ignorePos, $token[2] + $ignoreLine, $line]; + $line += substr_count($token->text, "\n"); + $pendingToken = [$text, $ignorePos, $token->line + $ignoreLine, $line]; } if ($pendingToken !== null) { @@ -250,7 +247,7 @@ private function getLinesToIgnoreForTokenByIgnoreComment( string $tokenText, int $tokenLine, string $ignoreComment, - bool $ignoreNextLine = false, + bool $ignoreNextLine, ): array { $lines = []; diff --git a/src/Parser/SimpleParser.php b/src/Parser/SimpleParser.php index efcf47d786..8fbd112742 100644 --- a/src/Parser/SimpleParser.php +++ b/src/Parser/SimpleParser.php @@ -9,12 +9,14 @@ use PHPStan\File\FileReader; use PHPStan\ShouldNotHappenException; -class SimpleParser implements Parser +final class SimpleParser implements Parser { public function __construct( private \PhpParser\Parser $parser, private NameResolver $nameResolver, + private VariadicMethodsVisitor $variadicMethodsVisitor, + private VariadicFunctionsVisitor $variadicFunctionsVisitor, ) { } @@ -48,6 +50,8 @@ public function parseString(string $sourceCode): array $nodeTraverser = new NodeTraverser(); $nodeTraverser->addVisitor($this->nameResolver); + $nodeTraverser->addVisitor($this->variadicMethodsVisitor); + $nodeTraverser->addVisitor($this->variadicFunctionsVisitor); /** @var array */ return $nodeTraverser->traverse($nodes); diff --git a/src/Parser/StandaloneThrowExprVisitor.php b/src/Parser/StandaloneThrowExprVisitor.php new file mode 100644 index 0000000000..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 index e6341a9de6..5c043161b5 100644 --- a/src/Parser/TraitCollectingVisitor.php +++ b/src/Parser/TraitCollectingVisitor.php @@ -2,6 +2,7 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; @@ -11,6 +12,7 @@ final class TraitCollectingVisitor extends NodeVisitorAbstract /** @var list */ public array $traits = []; + #[Override] public function enterNode(Node $node): ?Node { if (!$node instanceof Node\Stmt\Trait_) { diff --git a/src/Parser/TryCatchTypeVisitor.php b/src/Parser/TryCatchTypeVisitor.php index cca8bf4e3a..df2c8f4210 100644 --- a/src/Parser/TryCatchTypeVisitor.php +++ b/src/Parser/TryCatchTypeVisitor.php @@ -2,12 +2,15 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\DependencyInjection\AutowiredService; use function array_pop; use function array_reverse; use function count; +#[AutowiredService] final class TryCatchTypeVisitor extends NodeVisitorAbstract { @@ -16,12 +19,14 @@ final class TryCatchTypeVisitor extends NodeVisitorAbstract /** @var array|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_) { @@ -57,6 +62,7 @@ public function enterNode(Node $node): ?Node return null; } + #[Override] public function leaveNode(Node $node): ?Node { if ( diff --git a/src/Parser/TypeTraverserInstanceofVisitor.php b/src/Parser/TypeTraverserInstanceofVisitor.php index a39226bbb2..3ad2fa0f11 100644 --- a/src/Parser/TypeTraverserInstanceofVisitor.php +++ b/src/Parser/TypeTraverserInstanceofVisitor.php @@ -2,22 +2,27 @@ namespace PHPStan\Parser; +use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\DependencyInjection\AutowiredService; -class TypeTraverserInstanceofVisitor extends NodeVisitorAbstract +#[AutowiredService] +final class TypeTraverserInstanceofVisitor extends NodeVisitorAbstract { public const ATTRIBUTE_NAME = 'insideTypeTraverserMap'; private int $depth = 0; + #[Override] public function beforeTraverse(array $nodes): ?array { $this->depth = 0; return null; } + #[Override] public function enterNode(Node $node): ?Node { if ($node instanceof Node\Expr\Instanceof_ && $this->depth > 0) { @@ -38,6 +43,7 @@ public function enterNode(Node $node): ?Node return null; } + #[Override] public function leaveNode(Node $node): ?Node { if ( 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..4e5bf66d75 --- /dev/null +++ b/src/Parser/VariadicMethodsVisitor.php @@ -0,0 +1,140 @@ + */ + private array $classStack = []; + + private ?string $inMethod = null; + + /** @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->inMethod = 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\ClassLike) { + if (!$node->name instanceof Node\Identifier) { + $className = sprintf('%s:%s:%s', self::ANONYMOUS_CLASS_PREFIX, $node->getStartLine(), $node->getEndLine()); + $this->classStack[] = $className; + } else { + $className = $node->name->name; + $this->classStack[] = $this->inNamespace !== null ? $this->inNamespace . '\\' . $className : $className; + } + } + + if ($node instanceof ClassMethod) { + $this->inMethod = $node->name->name; + } + + if ( + $this->inMethod !== null + && $node instanceof Node\Expr\FuncCall + && $node->name instanceof Name + && in_array((string) $node->name, ParametersAcceptor::VARIADIC_FUNCTIONS, true) + ) { + $lastClass = $this->classStack[count($this->classStack) - 1] ?? null; + if ($lastClass !== null) { + if ( + !array_key_exists($lastClass, $this->variadicMethods) + || !array_key_exists($this->inMethod, $this->variadicMethods[$lastClass]) + ) { + $this->variadicMethods[$lastClass][$this->inMethod] = true; + } + } + + } + + return null; + } + + #[Override] + public function leaveNode(Node $node): ?Node + { + if ($node instanceof ClassMethod) { + $lastClass = $this->classStack[count($this->classStack) - 1] ?? null; + if ($lastClass !== null) { + $this->variadicMethods[$lastClass][$this->inMethod] ??= false; + } + $this->inMethod = null; + } + + if ($node instanceof Node\Stmt\ClassLike) { + array_pop($this->classStack); + } + + if ($node instanceof Node\Stmt\Namespace_ && $node->name !== null) { + $this->inNamespace = null; + } + + return null; + } + + #[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 8aa5e2435b..2a151b58ae 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -2,10 +2,14 @@ namespace PHPStan\Php; +use PHPStan\DependencyInjection\AutowiredService; use function floor; -/** @api */ -class PhpVersion +/** + * @api + */ +#[AutowiredService(factory: '@PHPStan\Php\PhpVersionFactory::create')] +final class PhpVersion { public const SOURCE_RUNTIME = 1; @@ -14,12 +18,22 @@ class PhpVersion public const SOURCE_UNKNOWN = 4; /** + * @api + * * @param self::SOURCE_* $source */ public function __construct(private int $versionId, private int $source = self::SOURCE_UNKNOWN) { } + /** + * @return self::SOURCE_* + */ + public function getSource(): int + { + return $this->source; + } + public function getSourceLabel(): string { switch ($this->source) { @@ -39,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 : ''); } @@ -259,6 +288,11 @@ public function castsNumbersToStringsOnLooseComparison(): bool return $this->versionId >= 80000; } + public function nonNumericStringAndIntegerIsFalseOnLooseComparison(): bool + { + return $this->versionId >= 80000; + } + public function supportsCallableInstanceMethods(): bool { return $this->versionId < 80000; @@ -322,6 +356,26 @@ public function supportsPregCaptureOnlyNamedGroups(): bool 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; @@ -335,4 +389,29 @@ public function isCurloptUrlCheckingFileSchemeWithOpenBasedir(): bool 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 02c0c8b749..73f510e0dc 100644 --- a/src/Php/PhpVersionFactory.php +++ b/src/Php/PhpVersionFactory.php @@ -2,14 +2,21 @@ 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 { + 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( private ?int $versionId, private ?string $composerPhpVersion, @@ -25,8 +32,8 @@ public function create(): PhpVersion } 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, 80399); + $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; diff --git a/src/Php/PhpVersionFactoryFactory.php b/src/Php/PhpVersionFactoryFactory.php index 870d1ff276..6ecfed8c71 100644 --- a/src/Php/PhpVersionFactoryFactory.php +++ b/src/Php/PhpVersionFactoryFactory.php @@ -4,21 +4,29 @@ 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 { /** + * @param int|array{min: int, max: int}|null $phpVersion * @param string[] $composerAutoloaderProjectPaths */ public function __construct( - private ?int $versionId, + #[AutowiredParameter] + private int|array|null $phpVersion, + #[AutowiredParameter] private array $composerAutoloaderProjectPaths, ) { @@ -43,7 +51,17 @@ public function create(): PhpVersionFactory } } - return new PhpVersionFactory($this->versionId, $composerPhpVersion); + $versionId = null; + + if (is_int($this->phpVersion)) { + $versionId = $this->phpVersion; + } + + if (is_array($this->phpVersion)) { + $versionId = $this->phpVersion['min']; + } + + return new PhpVersionFactory($versionId, $composerPhpVersion); } } diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php new file mode 100644 index 0000000000..96bf233209 --- /dev/null +++ b/src/Php/PhpVersions.php @@ -0,0 +1,46 @@ +phpVersions; + } + + public function supportsNoncapturingCatches(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; + } + + public function producesWarningForFinalPrivateMethods(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; + } + + public function supportsNamedArguments(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; + } + + public function supportsNamedArgumentAfterUnpackedArgument(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80100, null)->isSuperTypeOf($this->phpVersions)->result; + } + +} diff --git a/src/PhpDoc/ConstExprNodeResolver.php b/src/PhpDoc/ConstExprNodeResolver.php index 4719a4c8e7..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,22 +12,36 @@ 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\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\MixedType; +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) { @@ -52,22 +68,74 @@ public function resolve(ConstExprNode $node): Type 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/ConstExprParserFactory.php b/src/PhpDoc/ConstExprParserFactory.php deleted file mode 100644 index aa2ca2657d..0000000000 --- a/src/PhpDoc/ConstExprParserFactory.php +++ /dev/null @@ -1,19 +0,0 @@ -unescapeStrings, $this->unescapeStrings); - } - -} diff --git a/src/PhpDoc/CountableStubFilesExtension.php b/src/PhpDoc/CountableStubFilesExtension.php deleted file mode 100644 index af7761e625..0000000000 --- a/src/PhpDoc/CountableStubFilesExtension.php +++ /dev/null @@ -1,21 +0,0 @@ -bleedingEdge) { - return [__DIR__ . '/../../stubs/bleedingEdge/Countable.stub']; - } - - return [__DIR__ . '/../../stubs/Countable.stub']; - } - -} diff --git a/src/PhpDoc/DefaultStubFilesProvider.php b/src/PhpDoc/DefaultStubFilesProvider.php index 15919f8430..f694dcf9c3 100644 --- a/src/PhpDoc/DefaultStubFilesProvider.php +++ b/src/PhpDoc/DefaultStubFilesProvider.php @@ -2,6 +2,8 @@ namespace PHPStan\PhpDoc; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; use PHPStan\Internal\ComposerHelper; use function array_filter; @@ -9,7 +11,8 @@ use function str_contains; use function strtr; -class DefaultStubFilesProvider implements StubFilesProvider +#[AutowiredService(as: StubFilesProvider::class)] +final class DefaultStubFilesProvider implements StubFilesProvider { /** @var string[]|null */ @@ -24,7 +27,9 @@ class DefaultStubFilesProvider implements StubFilesProvider */ public function __construct( private Container $container, + #[AutowiredParameter] private array $stubFiles, + #[AutowiredParameter] private array $composerAutoloaderProjectPaths, ) { diff --git a/src/PhpDoc/DirectTypeNodeResolverExtensionRegistryProvider.php b/src/PhpDoc/DirectTypeNodeResolverExtensionRegistryProvider.php index cd912d76bb..c1c038ca81 100644 --- a/src/PhpDoc/DirectTypeNodeResolverExtensionRegistryProvider.php +++ b/src/PhpDoc/DirectTypeNodeResolverExtensionRegistryProvider.php @@ -2,7 +2,7 @@ namespace PHPStan\PhpDoc; -class DirectTypeNodeResolverExtensionRegistryProvider implements TypeNodeResolverExtensionRegistryProvider +final class DirectTypeNodeResolverExtensionRegistryProvider implements TypeNodeResolverExtensionRegistryProvider { public function __construct(private TypeNodeResolverExtensionRegistry $registry) diff --git a/src/PhpDoc/JsonValidateStubFilesExtension.php b/src/PhpDoc/JsonValidateStubFilesExtension.php index 19c1f824d0..61ba6aca5d 100644 --- a/src/PhpDoc/JsonValidateStubFilesExtension.php +++ b/src/PhpDoc/JsonValidateStubFilesExtension.php @@ -2,9 +2,11 @@ namespace PHPStan\PhpDoc; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; -class JsonValidateStubFilesExtension implements StubFilesExtension +#[AutowiredService] +final class JsonValidateStubFilesExtension implements StubFilesExtension { public function __construct(private PhpVersion $phpVersion) diff --git a/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php b/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php index 4a041e369e..eb0a48b337 100644 --- a/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php +++ b/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php @@ -2,9 +2,11 @@ namespace PHPStan\PhpDoc; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; -class LazyTypeNodeResolverExtensionRegistryProvider implements TypeNodeResolverExtensionRegistryProvider +#[AutowiredService(as: TypeNodeResolverExtensionRegistryProvider::class)] +final class LazyTypeNodeResolverExtensionRegistryProvider implements TypeNodeResolverExtensionRegistryProvider { private ?TypeNodeResolverExtensionRegistry $registry = null; @@ -15,14 +17,10 @@ public function __construct(private Container $container) public function getRegistry(): TypeNodeResolverExtensionRegistry { - if ($this->registry === null) { - $this->registry = new TypeNodeResolverExtensionAwareRegistry( - $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 @@ +trait; } - public function isExplicit(): bool - { - return $this->explicit; - } - /** * @return array */ @@ -113,63 +103,66 @@ public function transformAssertTagParameterWithParameterNameMapping(AssertTagPar return $parameter; } - /** - * @param array $originalPositionalParameterNames - * @param array $newPositionalParameterNames - */ public static function resolvePhpDocBlockForProperty( ?string $docComment, ClassReflection $classReflection, ?string $trait, string $propertyName, ?string $file, - ?bool $explicit, - array $originalPositionalParameterNames, // unused - array $newPositionalParameterNames, // unused ): self { - return self::resolvePhpDocBlockTree( - $docComment, + $docBlocksFromParents = []; + foreach (self::getParentReflections($classReflection) as $parentReflection) { + $oneResult = self::resolvePropertyPhpDocBlockFromClass( + $parentReflection, + $propertyName, + ); + + 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 array $originalPositionalParameterNames - * @param array $newPositionalParameterNames - */ public static function resolvePhpDocBlockForConstant( ?string $docComment, ClassReflection $classReflection, - ?string $trait, // unused string $constantName, ?string $file, - ?bool $explicit, - array $originalPositionalParameterNames, // unused - array $newPositionalParameterNames, // unused ): self { - return self::resolvePhpDocBlockTree( - $docComment, + $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, ); } @@ -183,60 +176,60 @@ public static function resolvePhpDocBlockForMethod( ?string $trait, string $methodName, ?string $file, - ?bool $explicit, array $originalPositionalParameterNames, array $newPositionalParameterNames, ): self { - return self::resolvePhpDocBlockTree( - $docComment, - $classReflection, - $trait, - $methodName, - $file, - 'hasNativeMethod', - 'getNativeMethod', - __FUNCTION__, - $explicit, - $originalPositionalParameterNames, - $newPositionalParameterNames, - ); - } + $docBlocksFromParents = []; + foreach (self::getParentReflections($classReflection) as $parentReflection) { + $oneResult = self::resolveMethodPhpDocBlockFromClass( + $parentReflection, + $methodName, + $newPositionalParameterNames, + ); - /** - * @param array $originalPositionalParameterNames - * @param array $newPositionalParameterNames - */ - private static function resolvePhpDocBlockTree( - ?string $docComment, - ClassReflection $classReflection, - ?string $trait, - string $name, - ?string $file, - string $hasMethodName, - string $getMethodName, - string $resolveMethodName, - ?bool $explicit, - array $originalPositionalParameterNames, - array $newPositionalParameterNames, - ): self - { - $docBlocksFromParents = self::resolveParentPhpDocBlocks( - $classReflection, - $name, - $hasMethodName, - $getMethodName, - $resolveMethodName, - $explicit ?? $docComment !== null, - $newPositionalParameterNames, - ); + if ($oneResult === null) { // Null if it is private or from a wrong trait. + continue; + } + + $docBlocksFromParents[] = $oneResult; + } + + foreach ($classReflection->getTraits(true) as $traitReflection) { + if (!$traitReflection->hasNativeMethod($methodName)) { + continue; + } + $traitMethod = $traitReflection->getNativeMethod($methodName); + $abstract = $traitMethod->isAbstract(); + if (is_bool($abstract)) { + if (!$abstract) { + continue; + } + } elseif (!$abstract->yes()) { + continue; + } + + $methodVariant = $traitMethod->getOnlyVariant(); + $positionalMethodParameterNames = []; + foreach ($methodVariant->getParameters() as $methodParameter) { + $positionalMethodParameterNames[] = $methodParameter->getName(); + } + + $docBlocksFromParents[] = new self( + $traitMethod->getDocComment() ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, + $classReflection->getFileName(), + $classReflection, + $traitReflection->getName(), + self::remapParameterNames($newPositionalParameterNames, $positionalMethodParameterNames), + [], + ); + } return new self( $docComment ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, $file, $classReflection, $trait, - $explicit ?? true, self::remapParameterNames($originalPositionalParameterNames, $newPositionalParameterNames), $docBlocksFromParents, ); @@ -263,44 +256,6 @@ private static function remapParameterNames( return $parameterNameMapping; } - /** - * @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; - } - /** * @return array */ @@ -320,67 +275,106 @@ 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 array $positionalParameterNames */ - private static function resolvePhpDocBlockFromClass( + private static function resolveMethodPhpDocBlockFromClass( ClassReflection $classReflection, string $name, - string $hasMethodName, - string $getMethodName, - string $resolveMethodName, - bool $explicit, array $positionalParameterNames, ): ?self { - if ($classReflection->$hasMethodName($name)) { - /** @var PropertyReflection|MethodReflection|ConstantReflection $parentReflection */ - $parentReflection = $classReflection->$getMethodName($name); + if ($classReflection->hasNativeMethod($name)) { + $parentReflection = $classReflection->getNativeMethod($name); if ($parentReflection->isPrivate()) { return null; } $classReflection = $parentReflection->getDeclaringClass(); - - if ($parentReflection instanceof PhpPropertyReflection || $parentReflection instanceof ResolvedPropertyReflection) { + $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( + return self::resolvePhpDocBlockForMethod( $parentReflection->getDocComment() ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, $classReflection, $trait, $name, $classReflection->getFileName(), - $explicit, $positionalParameterNames, $positionalMethodParameterNames, ); diff --git a/src/PhpDoc/PhpDocInheritanceResolver.php b/src/PhpDoc/PhpDocInheritanceResolver.php index 0e909dd96a..b72e7fe36b 100644 --- a/src/PhpDoc/PhpDocInheritanceResolver.php +++ b/src/PhpDoc/PhpDocInheritanceResolver.php @@ -3,12 +3,14 @@ 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 { public function __construct( @@ -32,9 +34,6 @@ public function resolvePhpDocForProperty( null, $propertyName, $classReflectionFileName, - null, - [], - [], ); return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, $declaringTraitName, null, $propertyName, null); @@ -50,12 +49,8 @@ public function resolvePhpDocForConstant( $phpDocBlock = PhpDocBlock::resolvePhpDocBlockForConstant( $docComment, $classReflection, - null, $constantName, $classReflectionFileName, - null, - [], - [], ); return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, null, null, null, $constantName); @@ -79,7 +74,6 @@ public function resolvePhpDocForMethod( $declaringTraitName, $methodName, $fileName, - null, $positionalParameterNames, $positionalParameterNames, ); diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index c11c26d7b0..ff91a44225 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -3,6 +3,7 @@ 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; @@ -18,6 +19,7 @@ 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; @@ -36,6 +38,7 @@ use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function array_key_exists; @@ -48,7 +51,8 @@ use function str_starts_with; use function substr; -class PhpDocNodeResolver +#[AutowiredService] +final class PhpDocNodeResolver { public function __construct( @@ -75,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; } } @@ -111,7 +115,6 @@ public function resolvePropertyTags(PhpDocNode $phpDocNode, NameScope $nameScope $resolved[$propertyName] = new PropertyTag( $propertyType, $propertyType, - $propertyType, ); } } @@ -127,7 +130,6 @@ public function resolvePropertyTags(PhpDocNode $phpDocNode, NameScope $nameScope } $resolved[$propertyName] = new PropertyTag( - $propertyType, $propertyType, $writableType, ); @@ -145,7 +147,6 @@ public function resolvePropertyTags(PhpDocNode $phpDocNode, NameScope $nameScope } $resolved[$propertyName] = new PropertyTag( - $readableType ?? $propertyType, $readableType, $propertyType, ); @@ -175,6 +176,9 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): $templateType->bound !== null ? $this->typeNodeResolver->resolve($templateType->bound, $nameScope) : new MixedType(), + $templateType->default !== null + ? $this->typeNodeResolver->resolve($templateType->default, $nameScope) + : null, TemplateTypeVariance::createInvariant(), ); } @@ -195,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( @@ -326,9 +330,12 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope } } + $nameScopeWithoutCurrent = $nameScope->unsetTemplateType($valueNode->name); + $resolved[$valueNode->name] = new TemplateTag( $valueNode->name, - $valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScope->unsetTemplateType($valueNode->name)) : new MixedType(true), + $valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScopeWithoutCurrent) : new MixedType(true), + $valueNode->default !== null ? $this->typeNodeResolver->resolve($valueNode->default, $nameScopeWithoutCurrent) : null, $variance, ); $resolvedPrefix[$valueNode->name] = $prefix; @@ -421,7 +428,12 @@ public function resolveParamClosureThisTags(PhpDocNode $phpDocNode, NameScope $n 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($this->typeNodeResolver->resolve($tagValue->type, $nameScope)); + $closureThisTypes[$parameterName] = new ParamClosureThisTag( + TypeCombinator::intersect( + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), + new ObjectWithoutClassType(), + ), + ); } } @@ -513,6 +525,24 @@ public function resolveRequireImplementsTags(PhpDocNode $phpDocNode, 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; + } + /** * @return array */ @@ -581,19 +611,19 @@ private function resolveAssertTagsFor(PhpDocNode $phpDocNode, NameScope $nameSco foreach ($phpDocNode->getAssertTagValues($tagName) as $assertTagValue) { $type = $this->typeNodeResolver->resolve($assertTagValue->type, $nameScope); $parameter = new AssertTagParameter($assertTagValue->parameter, null, null); - $resolved[] = new AssertTag($if, $type, $parameter, $assertTagValue->isNegated, $assertTagValue->isEquality ?? false, true); + $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 ?? false, true); + $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 ?? false, true); + $resolved[] = new AssertTag($if, $type, $parameter, $assertTagValue->isNegated, $assertTagValue->isEquality, true); } return $resolved; diff --git a/src/PhpDoc/PhpDocStringResolver.php b/src/PhpDoc/PhpDocStringResolver.php index d9d6203e5b..30a6e1df0e 100644 --- a/src/PhpDoc/PhpDocStringResolver.php +++ b/src/PhpDoc/PhpDocStringResolver.php @@ -2,12 +2,14 @@ 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 { public function __construct(private Lexer $phpDocLexer, private PhpDocParser $phpDocParser) 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 index 0fe8fbb5b2..d48519b735 100644 --- a/src/PhpDoc/ReflectionEnumStubFilesExtension.php +++ b/src/PhpDoc/ReflectionEnumStubFilesExtension.php @@ -2,9 +2,11 @@ namespace PHPStan\PhpDoc; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; -class ReflectionEnumStubFilesExtension implements StubFilesExtension +#[AutowiredService] +final class ReflectionEnumStubFilesExtension implements StubFilesExtension { public function __construct(private PhpVersion $phpVersion) @@ -17,7 +19,11 @@ public function getFiles(): array return []; } - return [__DIR__ . '/../../stubs/ReflectionEnum.stub']; + 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 4783a8f710..dc43e9af5e 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -16,6 +16,7 @@ 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; @@ -40,8 +41,10 @@ use function is_bool; use function substr; -/** @api */ -class ResolvedPhpDocBlock +/** + * @api + */ +final class ResolvedPhpDocBlock { public const EMPTY_DOC_STRING = '/** */'; @@ -109,6 +112,9 @@ class ResolvedPhpDocBlock /** @var array|false */ private array|false $requireImplementsTags = false; + /** @var array|false */ + private array|false $sealedTypeTags = false; + /** @var array|false */ private array|false $typeAliasTags = false; @@ -216,6 +222,7 @@ public static function createEmpty(): self $self->mixinTags = []; $self->requireExtendsTags = []; $self->requireImplementsTags = []; + $self->sealedTypeTags = []; $self->typeAliasTags = []; $self->typeAliasImportTags = []; $self->assertTags = []; @@ -280,6 +287,7 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $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->assertTags = self::mergeAssertTags($this->getAssertTags(), $parents, $parentPhpDocBlocks); @@ -661,6 +669,21 @@ public function getRequireImplementsTags(): array 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 */ @@ -731,12 +754,9 @@ public function getDeprecatedTag(): ?DeprecatedTag public function isDeprecated(): bool { - if ($this->isDeprecated === null) { - $this->isDeprecated = $this->phpDocNodeResolver->resolveIsDeprecated( - $this->phpDocNode, - ); - } - return $this->isDeprecated; + return $this->isDeprecated ??= $this->phpDocNodeResolver->resolveIsDeprecated( + $this->phpDocNode, + ); } /** @@ -744,52 +764,37 @@ public function isDeprecated(): bool */ public function isNotDeprecated(): bool { - if ($this->isNotDeprecated === null) { - $this->isNotDeprecated = $this->phpDocNodeResolver->resolveIsNotDeprecated( - $this->phpDocNode, - ); - } - return $this->isNotDeprecated; + return $this->isNotDeprecated ??= $this->phpDocNodeResolver->resolveIsNotDeprecated( + $this->phpDocNode, + ); } public function isInternal(): bool { - if ($this->isInternal === null) { - $this->isInternal = $this->phpDocNodeResolver->resolveIsInternal( - $this->phpDocNode, - ); - } - return $this->isInternal; + 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 { - if ($this->hasConsistentConstructor === null) { - $this->hasConsistentConstructor = $this->phpDocNodeResolver->resolveHasConsistentConstructor( - $this->phpDocNode, - ); - } - return $this->hasConsistentConstructor; + return $this->hasConsistentConstructor ??= $this->phpDocNodeResolver->resolveHasConsistentConstructor( + $this->phpDocNode, + ); } public function acceptsNamedArguments(): bool { - if ($this->acceptsNamedArguments === null) { - $this->acceptsNamedArguments = $this->phpDocNodeResolver->resolveAcceptsNamedArguments( - $this->phpDocNode, - ); - } - return $this->acceptsNamedArguments; + return $this->acceptsNamedArguments ??= $this->phpDocNodeResolver->resolveAcceptsNamedArguments( + $this->phpDocNode, + ); } public function getTemplateTypeMap(): TemplateTypeMap @@ -824,33 +829,23 @@ public function isPure(): ?bool public function isReadOnly(): bool { - if ($this->isReadOnly === null) { - $this->isReadOnly = $this->phpDocNodeResolver->resolveIsReadOnly( - $this->phpDocNode, - ); - } - return $this->isReadOnly; + return $this->isReadOnly ??= $this->phpDocNodeResolver->resolveIsReadOnly( + $this->phpDocNode, + ); } public function isImmutable(): bool { - if ($this->isImmutable === null) { - $this->isImmutable = $this->phpDocNodeResolver->resolveIsImmutable( - $this->phpDocNode, - ); - } - return $this->isImmutable; + return $this->isImmutable ??= $this->phpDocNodeResolver->resolveIsImmutable( + $this->phpDocNode, + ); } public function isAllowedPrivateMutation(): bool { - if ($this->isAllowedPrivateMutation === null) { - $this->isAllowedPrivateMutation = $this->phpDocNodeResolver->resolveAllowPrivateMutation( - $this->phpDocNode, - ); - } - - return $this->isAllowedPrivateMutation; + return $this->isAllowedPrivateMutation ??= $this->phpDocNodeResolver->resolveAllowPrivateMutation( + $this->phpDocNode, + ); } /** @@ -884,7 +879,7 @@ private static function mergeVarTags(array $varTags, array $parents, array $pare private static function mergeOneParentVarTags(self $parent, PhpDocBlock $phpDocBlock): ?array { foreach ($parent->getVarTags() as $key => $parentVarTag) { - return [$key => self::resolveTemplateTypeInTag($parentVarTag, $phpDocBlock, TemplateTypeVariance::createInvariant())]; + return [$key => self::resolveTemplateTypeInTag($parentVarTag->toImplicit(), $phpDocBlock, TemplateTypeVariance::createInvariant())]; } return null; diff --git a/src/PhpDoc/SocketSelectStubFilesExtension.php b/src/PhpDoc/SocketSelectStubFilesExtension.php index 1113a10f89..cd954ca6b0 100644 --- a/src/PhpDoc/SocketSelectStubFilesExtension.php +++ b/src/PhpDoc/SocketSelectStubFilesExtension.php @@ -2,9 +2,11 @@ namespace PHPStan\PhpDoc; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; -class SocketSelectStubFilesExtension implements StubFilesExtension +#[AutowiredService] +final class SocketSelectStubFilesExtension implements StubFilesExtension { public function __construct(private PhpVersion $phpVersion) diff --git a/src/PhpDoc/StubPhpDocProvider.php b/src/PhpDoc/StubPhpDocProvider.php index d00085e985..1982bd1da7 100644 --- a/src/PhpDoc/StubPhpDocProvider.php +++ b/src/PhpDoc/StubPhpDocProvider.php @@ -7,6 +7,8 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Interface_; use PhpParser\Node\Stmt\Trait_; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Parser\Parser; use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; @@ -14,7 +16,8 @@ use function array_map; use function is_string; -class StubPhpDocProvider +#[AutowiredService(name: 'stubPhpDocProvider')] +final class StubPhpDocProvider { /** @var array */ @@ -58,7 +61,9 @@ class StubPhpDocProvider private array $knownFunctionParameterNames = []; public function __construct( + #[AutowiredParameter(ref: '@stubParser')] private Parser $parser, + #[AutowiredParameter(ref: '@stubFileTypeMapper')] private FileTypeMapper $fileTypeMapper, private StubFilesProvider $stubFilesProvider, ) diff --git a/src/PhpDoc/StubSourceLocatorFactory.php b/src/PhpDoc/StubSourceLocatorFactory.php index 27384f41b1..6eca6b0bab 100644 --- a/src/PhpDoc/StubSourceLocatorFactory.php +++ b/src/PhpDoc/StubSourceLocatorFactory.php @@ -14,7 +14,7 @@ use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository; use function dirname; -class StubSourceLocatorFactory +final class StubSourceLocatorFactory { public function __construct( diff --git a/src/PhpDoc/StubValidator.php b/src/PhpDoc/StubValidator.php index d0628f435c..43b7f79171 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -6,8 +6,8 @@ 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; @@ -26,6 +26,19 @@ 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; @@ -35,6 +48,8 @@ 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; @@ -42,23 +57,41 @@ 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; @@ -70,12 +103,12 @@ use function count; use function sprintf; -class StubValidator +#[AutowiredService] +final class StubValidator { public function __construct( private DerivativeContainerFactory $derivativeContainerFactory, - private bool $duplicateStubs, ) { } @@ -90,7 +123,6 @@ public function validate(array $stubFiles, bool $debug): array return []; } - $originalBroker = Broker::getInstance(); $originalReflectionProvider = ReflectionProviderStaticAccessor::getInstance(); $originalPhpVersion = PhpVersionStaticAccessor::getInstance(); $container = $this->derivativeContainerFactory->create([ @@ -130,7 +162,7 @@ static function (): void { } $internalErrorMessage = sprintf('Internal error: %s', $e->getMessage()); - $errors[] = (new Error($internalErrorMessage, $stubFile, null, $e)) + $errors[] = (new Error($internalErrorMessage, $stubFile, canBeIgnored: $e)) ->withIdentifier('phpstan.internal') ->withMetadata([ InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), @@ -139,7 +171,6 @@ static function (): void { } } - Broker::registerInstance($originalBroker); ReflectionProviderStaticAccessor::registerInstance($originalReflectionProvider); PhpVersionStaticAccessor::registerInstance($originalPhpVersion); ObjectType::resetCaches(); @@ -164,20 +195,38 @@ private function getRuleRegistry(Container $container): RuleRegistry $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($classNameCheck, $reflectionProvider), - new ExistingClassesInInterfaceExtendsRule($classNameCheck, $reflectionProvider), - new ExistingClassInClassExtendsRule($classNameCheck, $reflectionProvider), - new ExistingClassInTraitUseRule($classNameCheck, $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, $classNameCheck, $unresolvableTypeHelper, $phpVersion, true, false), - new OverridingMethodRule($phpVersion, new MethodSignatureRule($phpClassReflectionExtension, true, true, $container->getParameter('featureToggles')['abstractTraitMethod']), true, new MethodParameterComparisonHelper($phpVersion, $container->getParameter('featureToggles')['genericPrototypeMessage']), $phpClassReflectionExtension, $container->getParameter('featureToggles')['genericPrototypeMessage'], $container->getParameter('featureToggles')['finalByPhpDoc'], $container->getParameter('checkMissingOverrideMethodAttribute')), + new 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($genericAncestorsCheck, $crossCheckInterfacesHelper), @@ -187,40 +236,54 @@ private function getRuleRegistry(Container $container): RuleRegistry 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, $genericCallableRuleHelper), + new IncompatiblePhpDocTypeRule($fileTypeMapper, new IncompatiblePhpDocTypeCheck($genericObjectTypeCheck, $unresolvableTypeHelper, $genericCallableRuleHelper)), new IncompatiblePropertyPhpDocTypeRule($genericObjectTypeCheck, $unresolvableTypeHelper, $genericCallableRuleHelper), new InvalidPhpDocTagValueRule( $container->getByType(Lexer::class), $container->getByType(PhpDocParser::class), - $container->getParameter('featureToggles')['allInvalidPhpDocs'], - $container->getParameter('featureToggles')['invalidPhpDocTagLine'], + ), + new 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, $container->getParameter('featureToggles')['paramOutType']), + new MissingFunctionParameterTypehintRule($missingTypehintCheck), new MissingFunctionReturnTypehintRule($missingTypehintCheck), - new MissingMethodParameterTypehintRule($missingTypehintCheck, $container->getParameter('featureToggles')['paramOutType']), + new MissingMethodParameterTypehintRule($missingTypehintCheck), new MissingMethodReturnTypehintRule($missingTypehintCheck), new MissingPropertyTypehintRule($missingTypehintCheck), - ]; + new MissingMethodSelfOutTypeRule($missingTypehintCheck), - if ($this->duplicateStubs) { - $reflector = $container->getService('stubReflector'); - $relativePathHelper = $container->getService('simpleRelativePathHelper'); - $rules[] = new DuplicateClassDeclarationRule($reflector, $relativePathHelper); - $rules[] = new DuplicateFunctionDeclarationRule($reflector, $relativePathHelper); - } - - if ((bool) $container->getParameter('featureToggles')['allInvalidPhpDocs']) { - $rules[] = new InvalidPHPStanDocTagRule( - $container->getByType(Lexer::class), - $container->getByType(PhpDocParser::class), - true, - ); - } + // duplicate stubs + new DuplicateClassDeclarationRule($reflector, $relativePathHelper), + new DuplicateFunctionDeclarationRule($reflector, $relativePathHelper), + ]; return new DirectRuleRegistry($rules); } diff --git a/src/PhpDoc/Tag/AssertTagParameter.php b/src/PhpDoc/Tag/AssertTagParameter.php index 18717b3494..a3c3250326 100644 --- a/src/PhpDoc/Tag/AssertTagParameter.php +++ b/src/PhpDoc/Tag/AssertTagParameter.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr; use function sprintf; -class AssertTagParameter +final class AssertTagParameter { public function __construct( diff --git a/src/PhpDoc/Tag/DeprecatedTag.php b/src/PhpDoc/Tag/DeprecatedTag.php index ea798c2aab..9bc036e1d8 100644 --- a/src/PhpDoc/Tag/DeprecatedTag.php +++ b/src/PhpDoc/Tag/DeprecatedTag.php @@ -2,8 +2,10 @@ namespace PHPStan\PhpDoc\Tag; -/** @api */ -class DeprecatedTag +/** + * @api + */ +final class DeprecatedTag { public function __construct(private ?string $message) diff --git a/src/PhpDoc/Tag/ExtendsTag.php b/src/PhpDoc/Tag/ExtendsTag.php index 74ed32b7a2..72cb97f7cf 100644 --- a/src/PhpDoc/Tag/ExtendsTag.php +++ b/src/PhpDoc/Tag/ExtendsTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class ExtendsTag +/** + * @api + */ +final class ExtendsTag { public function __construct(private Type $type) diff --git a/src/PhpDoc/Tag/ImplementsTag.php b/src/PhpDoc/Tag/ImplementsTag.php index cc9376b47a..556959b68d 100644 --- a/src/PhpDoc/Tag/ImplementsTag.php +++ b/src/PhpDoc/Tag/ImplementsTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class ImplementsTag +/** + * @api + */ +final class ImplementsTag { public function __construct(private Type $type) diff --git a/src/PhpDoc/Tag/MethodTag.php b/src/PhpDoc/Tag/MethodTag.php index 9f46c124d2..43bda4cf97 100644 --- a/src/PhpDoc/Tag/MethodTag.php +++ b/src/PhpDoc/Tag/MethodTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class MethodTag +/** + * @api + */ +final class MethodTag { /** @@ -16,7 +18,7 @@ public function __construct( private Type $returnType, private bool $isStatic, private array $parameters, - private array $templateTags = [], + private array $templateTags, ) { } diff --git a/src/PhpDoc/Tag/MethodTagParameter.php b/src/PhpDoc/Tag/MethodTagParameter.php index 1326c4cbc9..3e4c817bf8 100644 --- a/src/PhpDoc/Tag/MethodTagParameter.php +++ b/src/PhpDoc/Tag/MethodTagParameter.php @@ -5,8 +5,10 @@ use PHPStan\Reflection\PassedByReference; use PHPStan\Type\Type; -/** @api */ -class MethodTagParameter +/** + * @api + */ +final class MethodTagParameter { public function __construct( diff --git a/src/PhpDoc/Tag/MixinTag.php b/src/PhpDoc/Tag/MixinTag.php index 2a97b73264..c115c2cacb 100644 --- a/src/PhpDoc/Tag/MixinTag.php +++ b/src/PhpDoc/Tag/MixinTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class MixinTag +/** + * @api + */ +final class MixinTag { public function __construct(private Type $type) diff --git a/src/PhpDoc/Tag/ParamClosureThisTag.php b/src/PhpDoc/Tag/ParamClosureThisTag.php index 1dacb4c41d..92a91a4da8 100644 --- a/src/PhpDoc/Tag/ParamClosureThisTag.php +++ b/src/PhpDoc/Tag/ParamClosureThisTag.php @@ -4,7 +4,9 @@ use PHPStan\Type\Type; -/** @api */ +/** + * @api + */ final class ParamClosureThisTag implements TypedTag { @@ -19,10 +21,7 @@ public function getType(): Type return $this->type; } - /** - * @return self - */ - public function withType(Type $type): TypedTag + public function withType(Type $type): self { return new self($type); } diff --git a/src/PhpDoc/Tag/ParamOutTag.php b/src/PhpDoc/Tag/ParamOutTag.php index c40018cb45..f720897deb 100644 --- a/src/PhpDoc/Tag/ParamOutTag.php +++ b/src/PhpDoc/Tag/ParamOutTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class ParamOutTag implements TypedTag +/** + * @api + */ +final class ParamOutTag implements TypedTag { public function __construct(private Type $type) @@ -17,10 +19,7 @@ public function getType(): Type return $this->type; } - /** - * @return self - */ - public function withType(Type $type): TypedTag + public function withType(Type $type): self { return new self($type); } diff --git a/src/PhpDoc/Tag/ParamTag.php b/src/PhpDoc/Tag/ParamTag.php index 8515df30b9..498dd64ce7 100644 --- a/src/PhpDoc/Tag/ParamTag.php +++ b/src/PhpDoc/Tag/ParamTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class ParamTag implements TypedTag +/** + * @api + */ +final class ParamTag implements TypedTag { public function __construct( @@ -25,10 +27,7 @@ public function isVariadic(): bool return $this->isVariadic; } - /** - * @return self - */ - public function withType(Type $type): TypedTag + public function withType(Type $type): self { return new self($type, $this->isVariadic); } diff --git a/src/PhpDoc/Tag/PropertyTag.php b/src/PhpDoc/Tag/PropertyTag.php index f2e42c14b3..16090c44b0 100644 --- a/src/PhpDoc/Tag/PropertyTag.php +++ b/src/PhpDoc/Tag/PropertyTag.php @@ -4,26 +4,19 @@ use PHPStan\Type\Type; -/** @api */ -class PropertyTag +/** + * @api + */ +final class PropertyTag { public function __construct( - private Type $type, private ?Type $readableType, private ?Type $writableType, ) { } - /** - * @deprecated Use getReadableType() / getWritableType() - */ - public function getType(): Type - { - return $this->type; - } - public function getReadableType(): ?Type { return $this->readableType; diff --git a/src/PhpDoc/Tag/RequireExtendsTag.php b/src/PhpDoc/Tag/RequireExtendsTag.php index a1e60d45a6..97bf685468 100644 --- a/src/PhpDoc/Tag/RequireExtendsTag.php +++ b/src/PhpDoc/Tag/RequireExtendsTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class RequireExtendsTag +/** + * @api + */ +final class RequireExtendsTag { public function __construct(private Type $type) diff --git a/src/PhpDoc/Tag/RequireImplementsTag.php b/src/PhpDoc/Tag/RequireImplementsTag.php index 2a0f42303f..aafd560260 100644 --- a/src/PhpDoc/Tag/RequireImplementsTag.php +++ b/src/PhpDoc/Tag/RequireImplementsTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class RequireImplementsTag +/** + * @api + */ +final class RequireImplementsTag { public function __construct(private Type $type) diff --git a/src/PhpDoc/Tag/ReturnTag.php b/src/PhpDoc/Tag/ReturnTag.php index 683b0cb259..b501dd67e1 100644 --- a/src/PhpDoc/Tag/ReturnTag.php +++ b/src/PhpDoc/Tag/ReturnTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class ReturnTag implements TypedTag +/** + * @api + */ +final class ReturnTag implements TypedTag { public function __construct(private Type $type, private bool $isExplicit) @@ -22,10 +24,7 @@ public function isExplicit(): bool return $this->isExplicit; } - /** - * @return self - */ - public function withType(Type $type): TypedTag + public function withType(Type $type): self { return new self($type, $this->isExplicit); } diff --git a/src/PhpDoc/Tag/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 index bbd5a2ca09..10bb054179 100644 --- a/src/PhpDoc/Tag/SelfOutTypeTag.php +++ b/src/PhpDoc/Tag/SelfOutTypeTag.php @@ -4,7 +4,10 @@ use PHPStan\Type\Type; -class SelfOutTypeTag implements TypedTag +/** + * @api + */ +final class SelfOutTypeTag implements TypedTag { public function __construct(private Type $type) @@ -16,10 +19,7 @@ public function getType(): Type return $this->type; } - /** - * @return self - */ - public function withType(Type $type): TypedTag + public function withType(Type $type): self { return new self($type); } diff --git a/src/PhpDoc/Tag/TemplateTag.php b/src/PhpDoc/Tag/TemplateTag.php index 78707555c8..bafa555833 100644 --- a/src/PhpDoc/Tag/TemplateTag.php +++ b/src/PhpDoc/Tag/TemplateTag.php @@ -5,14 +5,16 @@ use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Type; -/** @api */ -class TemplateTag +/** + * @api + */ +final class TemplateTag { /** * @param non-empty-string $name */ - public function __construct(private string $name, private Type $bound, private TemplateTypeVariance $variance) + public function __construct(private string $name, private Type $bound, private ?Type $default, private TemplateTypeVariance $variance) { } @@ -29,6 +31,11 @@ public function getBound(): Type return $this->bound; } + public function getDefault(): ?Type + { + return $this->default; + } + public function getVariance(): TemplateTypeVariance { return $this->variance; diff --git a/src/PhpDoc/Tag/ThrowsTag.php b/src/PhpDoc/Tag/ThrowsTag.php index 15c1ac94d9..1c1e30b897 100644 --- a/src/PhpDoc/Tag/ThrowsTag.php +++ b/src/PhpDoc/Tag/ThrowsTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class ThrowsTag +/** + * @api + */ +final class ThrowsTag { public function __construct(private Type $type) diff --git a/src/PhpDoc/Tag/TypeAliasTag.php b/src/PhpDoc/Tag/TypeAliasTag.php index 35b613417a..d5cd10e5d6 100644 --- a/src/PhpDoc/Tag/TypeAliasTag.php +++ b/src/PhpDoc/Tag/TypeAliasTag.php @@ -6,8 +6,10 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Type\TypeAlias; -/** @api */ -class TypeAliasTag +/** + * @api + */ +final class TypeAliasTag { public function __construct( diff --git a/src/PhpDoc/Tag/UsesTag.php b/src/PhpDoc/Tag/UsesTag.php index 453eb5b250..1679997ed3 100644 --- a/src/PhpDoc/Tag/UsesTag.php +++ b/src/PhpDoc/Tag/UsesTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class UsesTag +/** + * @api + */ +final class UsesTag { public function __construct(private Type $type) diff --git a/src/PhpDoc/Tag/VarTag.php b/src/PhpDoc/Tag/VarTag.php index 672cb81d43..9d594fd75c 100644 --- a/src/PhpDoc/Tag/VarTag.php +++ b/src/PhpDoc/Tag/VarTag.php @@ -4,11 +4,13 @@ use PHPStan\Type\Type; -/** @api */ -class VarTag implements TypedTag +/** + * @api + */ +final class VarTag implements TypedTag { - public function __construct(private Type $type) + public function __construct(private Type $type, private bool $isExplicit) { } @@ -17,12 +19,19 @@ public function getType(): Type return $this->type; } - /** - * @return self - */ - public function withType(Type $type): TypedTag + public function withType(Type $type): self { - return new self($type); + return new self($type, $this->isExplicit); + } + + public function isExplicit(): bool + { + return $this->isExplicit; + } + + public function toImplicit(): self + { + return new self($this->type, false); } } diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 6a8a409e56..bbd3d0929a 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -10,6 +10,7 @@ use PhpParser\Node\Name; use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\NameScope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode; @@ -46,9 +47,11 @@ 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; @@ -58,6 +61,7 @@ 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; @@ -68,6 +72,7 @@ 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; @@ -105,6 +110,7 @@ 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; @@ -119,7 +125,8 @@ use function strtolower; use function substr; -class TypeNodeResolver +#[AutowiredService] +final class TypeNodeResolver { /** @var array */ @@ -194,7 +201,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': @@ -216,18 +231,25 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco ]); case 'string': - case 'lowercase-string': return new StringType(); + case 'lowercase-string': + return new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); + + case 'uppercase-string': + return new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]); + case 'literal-string': return new IntersectionType([new StringType(), new AccessoryLiteralStringType()]); case 'class-string': case 'interface-string': case 'trait-string': - case 'enum-string': return new ClassStringType(); + case 'enum-string': + return new GenericClassStringType(new ObjectType('UnitEnum')); + case 'callable-string': return new IntersectionType([new StringType(), new CallableType()]); @@ -287,10 +309,23 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco ]); case 'non-empty-string': + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + case 'non-empty-lowercase-string': return new IntersectionType([ new StringType(), new AccessoryNonEmptyStringType(), + new AccessoryLowercaseStringType(), + ]); + + case 'non-empty-uppercase-string': + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + new AccessoryUppercaseStringType(), ]); case 'truthy-string': @@ -357,7 +392,7 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco return new CallableType(); case 'pure-callable': - return new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()); + return new CallableType(isPure: TrinaryLogic::createYes()); case 'pure-closure': return ClosureType::createPure(); @@ -409,15 +444,11 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco return new NonAcceptingNeverType(); case 'list': - return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new MixedType())); + return TypeCombinator::intersect(new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType()), new AccessoryArrayListType()); case 'non-empty-list': - return AccessoryArrayListType::intersectWith(TypeCombinator::intersect( - new ArrayType(new IntegerType(), new MixedType()), - new NonEmptyArrayType(), - )); - case '__always-list': return TypeCombinator::intersect( - new ArrayType(new IntegerType(), new MixedType()), + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType()), + new NonEmptyArrayType(), new AccessoryArrayListType(), ); @@ -562,10 +593,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 { @@ -641,11 +674,21 @@ static function (string $variance): TemplateTypeVariance { 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->toArrayKey(), $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(); } @@ -657,7 +700,7 @@ static function (string $variance): TemplateTypeVariance { return $arrayType; } elseif (in_array($mainTypeName, ['list', 'non-empty-list'], true)) { if (count($genericTypes) === 1) { // list - $listType = AccessoryArrayListType::intersectWith(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()); } @@ -683,6 +726,13 @@ static function (string $variance): TemplateTypeVariance { } } + 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> @@ -763,6 +813,14 @@ static function (string $variance): TemplateTypeVariance { 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(); } @@ -778,11 +836,20 @@ static function (string $variance): TemplateTypeVariance { if ($mainTypeClassName !== null) { if (!$this->getReflectionProvider()->hasClass($mainTypeClassName)) { - return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); + return new GenericObjectType($mainTypeClassName, $genericTypes, variances: $variances); } $classReflection = $this->getReflectionProvider()->getClass($mainTypeClassName); if ($classReflection->isGeneric()) { + $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, @@ -792,7 +859,7 @@ static function (string $variance): TemplateTypeVariance { return new GenericObjectType($mainTypeClassName, [ new MixedType(true), $genericTypes[0], - ], null, null, [ + ], variances: [ TemplateTypeVariance::createInvariant(), $variances[0], ]); @@ -802,7 +869,7 @@ static function (string $variance): TemplateTypeVariance { return new GenericObjectType($mainTypeClassName, [ $genericTypes[0], $genericTypes[1], - ], null, null, [ + ], variances: [ $variances[0], $variances[1], ]); @@ -816,7 +883,7 @@ static function (string $variance): TemplateTypeVariance { $genericTypes[0], $mixed, $mixed, - ], null, null, [ + ], variances: [ TemplateTypeVariance::createInvariant(), $variances[0], TemplateTypeVariance::createInvariant(), @@ -831,7 +898,7 @@ static function (string $variance): TemplateTypeVariance { $genericTypes[1], $mixed, $mixed, - ], null, null, [ + ], variances: [ $variances[0], $variances[1], TemplateTypeVariance::createInvariant(), @@ -841,14 +908,14 @@ static function (string $variance): TemplateTypeVariance { } if (!$mainType->isIterable()->yes()) { - return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); + return new GenericObjectType($mainTypeClassName, $genericTypes, variances: $variances); } if ( count($genericTypes) !== 1 || $classReflection->getTemplateTypeMap()->count() === 1 ) { - return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); + return new GenericObjectType($mainTypeClassName, $genericTypes, variances: $variances); } } } @@ -884,7 +951,7 @@ static function (string $variance): TemplateTypeVariance { } if ($mainTypeClassName !== null) { - return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); + return new GenericObjectType($mainTypeClassName, $genericTypes, variances: $variances); } return new ErrorType(); @@ -894,13 +961,16 @@ private function resolveCallableTypeNode(CallableTypeNode $typeNode, NameScope $ { $templateTags = []; - if (count($typeNode->templateTypes ?? []) > 0) { + 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(), ); } @@ -919,7 +989,7 @@ private function resolveCallableTypeNode(CallableTypeNode $typeNode, NameScope $ $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; @@ -937,7 +1007,7 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi ); }, $typeNode->parameters, - ); + )); $returnType = $this->resolve($typeNode->returnType, $nameScope); @@ -947,13 +1017,13 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi return new ErrorType(); } - return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap, null, $templateTags, $pure); + return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap, templateTags: $templateTags, isPure: $pure); } elseif ( $mainType instanceof ObjectType && $mainType->getClassName() === Closure::class ) { - return new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, null, null, $templateTags, [], [ + return new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, templateTags: $templateTags, impurePoints: [ new SimpleImpurePoint( 'functionCall', 'call to a Closure', @@ -961,7 +1031,7 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi ), ]); } elseif ($mainType instanceof ClosureType) { - $closure = new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, null, null, $templateTags, [], $mainType->getImpurePoints()); + $closure = new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, templateTags: $templateTags, impurePoints: $mainType->getImpurePoints(), invalidateExpressions: $mainType->getInvalidateExpressions(), usedVariables: $mainType->getUsedVariables(), acceptsNamedArguments: $mainType->acceptsNamedArguments()); if ($closure->isPure()->yes() && $returnType->isVoid()->yes()) { return new ErrorType(); } @@ -994,8 +1064,18 @@ private function resolveArrayShapeNode(ArrayShapeNode $typeNode, NameScope $name } $arrayType = $builder->getArray(); - if ($typeNode->kind === ArrayShapeNode::KIND_LIST) { - $arrayType = AccessoryArrayListType::intersectWith($arrayType); + 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; @@ -1059,6 +1139,7 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc $className = $classReflection->getParentClass()->getName(); } + break; } } @@ -1181,13 +1262,17 @@ private function expandIntMaskToType(Type $type): ?Type 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 - * @return Type[] + * @return list */ public function resolveMultiple(array $typeNodes, NameScope $nameScope): array { diff --git a/src/PhpDoc/TypeNodeResolverExtensionAwareRegistry.php b/src/PhpDoc/TypeNodeResolverExtensionAwareRegistry.php index a41b9c631c..a525078b2e 100644 --- a/src/PhpDoc/TypeNodeResolverExtensionAwareRegistry.php +++ b/src/PhpDoc/TypeNodeResolverExtensionAwareRegistry.php @@ -2,7 +2,7 @@ namespace PHPStan\PhpDoc; -class TypeNodeResolverExtensionAwareRegistry implements TypeNodeResolverExtensionRegistry +final class TypeNodeResolverExtensionAwareRegistry implements TypeNodeResolverExtensionRegistry { /** diff --git a/src/PhpDoc/TypeStringResolver.php b/src/PhpDoc/TypeStringResolver.php index 91333363ed..9aa0b151bc 100644 --- a/src/PhpDoc/TypeStringResolver.php +++ b/src/PhpDoc/TypeStringResolver.php @@ -3,12 +3,14 @@ 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 { public function __construct(private Lexer $typeLexer, private TypeParser $typeParser, private TypeNodeResolver $typeNodeResolver) diff --git a/src/Process/CpuCoreCounter.php b/src/Process/CpuCoreCounter.php index cff6084b87..a0558bcbeb 100644 --- a/src/Process/CpuCoreCounter.php +++ b/src/Process/CpuCoreCounter.php @@ -4,8 +4,10 @@ use Fidry\CpuCoreCounter\CpuCoreCounter as FidryCpuCoreCounter; use Fidry\CpuCoreCounter\NumberOfCpuCoreNotFound; +use PHPStan\DependencyInjection\AutowiredService; -class CpuCoreCounter +#[AutowiredService] +final class CpuCoreCounter { private ?int $count = null; diff --git a/src/Process/ProcessCanceledException.php b/src/Process/ProcessCanceledException.php index 9d37c5b6ca..ae42c75d3b 100644 --- a/src/Process/ProcessCanceledException.php +++ b/src/Process/ProcessCanceledException.php @@ -4,7 +4,7 @@ use Exception; -class ProcessCanceledException extends Exception +final class ProcessCanceledException extends Exception { } diff --git a/src/Process/ProcessCrashedException.php b/src/Process/ProcessCrashedException.php index d6278a50e7..fb75a7d94d 100644 --- a/src/Process/ProcessCrashedException.php +++ b/src/Process/ProcessCrashedException.php @@ -4,7 +4,7 @@ use Exception; -class ProcessCrashedException extends Exception +final class ProcessCrashedException extends Exception { } diff --git a/src/Process/ProcessHelper.php b/src/Process/ProcessHelper.php index 2597e771a8..591e916fee 100644 --- a/src/Process/ProcessHelper.php +++ b/src/Process/ProcessHelper.php @@ -13,7 +13,7 @@ use function sprintf; use const PHP_BINARY; -class ProcessHelper +final class ProcessHelper { /** diff --git a/src/Process/ProcessPromise.php b/src/Process/ProcessPromise.php index 55e3cbcf2e..31c9f0b761 100644 --- a/src/Process/ProcessPromise.php +++ b/src/Process/ProcessPromise.php @@ -12,7 +12,7 @@ use function stream_get_contents; use function tmpfile; -class ProcessPromise +final class ProcessPromise { /** @var Deferred */ @@ -22,14 +22,11 @@ class ProcessPromise private bool $canceled = false; - public function __construct(private LoopInterface $loop, private string $name, private string $command) + public function __construct(private LoopInterface $loop, private string $command) { - $this->deferred = new Deferred(); - } - - public function getName(): string - { - return $this->name; + $this->deferred = new Deferred(function (): void { + $this->cancel(); + }); } /** @@ -46,7 +43,7 @@ public function run(): PromiseInterface 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, ]); @@ -72,9 +69,6 @@ public function run(): PromiseInterface } if ($exitCode === 0) { - if ($stdOut === false) { - $stdOut = ''; - } $this->deferred->resolve($stdOut); return; } @@ -85,7 +79,7 @@ public function run(): PromiseInterface return $this->deferred->promise(); } - public function cancel(): void + private function cancel(): void { if ($this->process === null) { throw new ShouldNotHappenException('Cancelling process before running'); diff --git a/src/Reflection/Annotations/AnnotationMethodReflection.php b/src/Reflection/Annotations/AnnotationMethodReflection.php index cfd5b74e03..9b1886b544 100644 --- a/src/Reflection/Annotations/AnnotationMethodReflection.php +++ b/src/Reflection/Annotations/AnnotationMethodReflection.php @@ -5,22 +5,23 @@ use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedFunctionVariant; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; +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 ExtendedMethodReflection +final class AnnotationMethodReflection implements ExtendedMethodReflection { - /** @var FunctionVariantWithPhpDocs[]|null */ + /** @var list|null */ private ?array $variants = null; /** - * @param AnnotationsMethodParameterReflection[] $parameters + * @param list $parameters */ public function __construct( private string $name, @@ -67,20 +68,22 @@ public function getName(): string public function getVariants(): array { - if ($this->variants === null) { - $this->variants = [ - new FunctionVariantWithPhpDocs( - $this->templateTypeMap, - null, - $this->parameters, - $this->isVariadic, - $this->returnType, - $this->returnType, - new MixedType(), - ), - ]; - } - 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 @@ -113,6 +116,11 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function getThrowType(): ?Type { return $this->throwType; @@ -141,6 +149,11 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->declaringClass->acceptsNamedArguments()); + } + public function getSelfOutType(): ?Type { return null; @@ -165,4 +178,9 @@ public function isPure(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getAttributes(): array + { + return []; + } + } diff --git a/src/Reflection/Annotations/AnnotationPropertyReflection.php b/src/Reflection/Annotations/AnnotationPropertyReflection.php index 6fe9b9d125..a79ffaaf3b 100644 --- a/src/Reflection/Annotations/AnnotationPropertyReflection.php +++ b/src/Reflection/Annotations/AnnotationPropertyReflection.php @@ -3,14 +3,18 @@ namespace PHPStan\Reflection\Annotations; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; -class AnnotationPropertyReflection implements PropertyReflection +final class AnnotationPropertyReflection implements ExtendedPropertyReflection { public function __construct( + private string $name, private ClassReflection $declaringClass, private Type $readableType, private Type $writableType, @@ -20,6 +24,11 @@ public function __construct( { } + public function getName(): string + { + return $this->name; + } + public function getDeclaringClass(): ClassReflection { return $this->declaringClass; @@ -40,6 +49,26 @@ 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->readableType; @@ -85,4 +114,49 @@ 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 []; + } + } diff --git a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php index fac7e5a9bb..b01a6db6ff 100644 --- a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php +++ b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php @@ -2,13 +2,13 @@ namespace PHPStan\Reflection\Annotations; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; +use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; use PHPStan\Type\Type; -class AnnotationsMethodParameterReflection implements ParameterReflectionWithPhpDocs +final class AnnotationsMethodParameterReflection implements ExtendedParameterReflection { public function __construct(private string $name, private Type $type, private PassedByReference $passedByReference, private bool $isOptional, private bool $isVariadic, private ?Type $defaultValue) @@ -35,6 +35,11 @@ public function getPhpDocType(): Type return $this->type; } + public function hasNativeType(): bool + { + return false; + } + public function getNativeType(): Type { return new MixedType(); @@ -70,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 2860988c91..9ad470b0ee 100644 --- a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php @@ -5,7 +5,6 @@ use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeHelper; @@ -16,7 +15,7 @@ use function array_map; use function count; -class AnnotationsMethodsClassReflectionExtension implements MethodsClassReflectionExtension +final class AnnotationsMethodsClassReflectionExtension implements MethodsClassReflectionExtension { /** @var ExtendedMethodReflection[][] */ @@ -35,10 +34,7 @@ public function hasMethod(ClassReflection $classReflection, string $methodName): return isset($this->methods[$classReflection->getCacheKey()][$methodName]); } - /** - * @return ExtendedMethodReflection - */ - public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection + public function getMethod(ClassReflection $classReflection, string $methodName): ExtendedMethodReflection { return $this->methods[$classReflection->getCacheKey()][$methodName]; } @@ -108,15 +104,6 @@ private function findClassReflectionWithMethod( return $methodWithDeclaringClass; } - foreach ($parentClass->getTraits() as $traitClass) { - $parentTraitMethodWithDeclaringClass = $this->findClassReflectionWithMethod($traitClass, $parentClass, $methodName); - if ($parentTraitMethodWithDeclaringClass === null) { - continue; - } - - return $parentTraitMethodWithDeclaringClass; - } - $parentClass = $parentClass->getParentClass(); } diff --git a/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php b/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php index 3ef4959ab8..5c19c29ae7 100644 --- a/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php @@ -3,16 +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 @@ -28,7 +28,7 @@ public function hasProperty(ClassReflection $classReflection, string $propertyNa return isset($this->properties[$classReflection->getCacheKey()][$propertyName]); } - public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection + public function getProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection { return $this->properties[$classReflection->getCacheKey()][$propertyName]; } @@ -37,7 +37,7 @@ private function findClassReflectionWithProperty( ClassReflection $classReflection, ClassReflection $declaringClass, string $propertyName, - ): ?PropertyReflection + ): ?ExtendedPropertyReflection { $propertyTags = $classReflection->getPropertyTags(); if (isset($propertyTags[$propertyName])) { @@ -52,6 +52,7 @@ private function findClassReflectionWithProperty( } return new AnnotationPropertyReflection( + $propertyName, $declaringClass, TemplateTypeHelper::resolveTemplateTypes( $propertyTag->getReadableType() ?? new NeverType(), @@ -86,15 +87,6 @@ private function findClassReflectionWithProperty( return $methodWithDeclaringClass; } - foreach ($parentClass->getTraits() as $traitClass) { - $parentTraitMethodWithDeclaringClass = $this->findClassReflectionWithProperty($traitClass, $parentClass, $propertyName); - if ($parentTraitMethodWithDeclaringClass === null) { - continue; - } - - return $parentTraitMethodWithDeclaringClass; - } - $parentClass = $parentClass->getParentClass(); } diff --git a/src/Reflection/Assertions.php b/src/Reflection/Assertions.php index 6adb97136c..a1f7ebfa6d 100644 --- a/src/Reflection/Assertions.php +++ b/src/Reflection/Assertions.php @@ -13,7 +13,7 @@ /** * @api */ -class Assertions +final class Assertions { private static ?self $empty = null; 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 cd18c1bfe3..4bea8a1200 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProvider.php +++ b/src/Reflection/BetterReflection/BetterReflectionProvider.php @@ -20,6 +20,8 @@ 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; @@ -31,15 +33,18 @@ 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; @@ -50,13 +55,16 @@ use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; use function array_key_exists; +use function array_key_first; use function array_map; use function base64_decode; +use function in_array; use function sprintf; use function strtolower; use const PHP_VERSION_ID; -class BetterReflectionProvider implements ReflectionProvider +#[NonAutowiredService(name: 'betterReflectionProvider')] +final class BetterReflectionProvider implements ReflectionProvider { /** @var FunctionReflection[] */ @@ -68,19 +76,21 @@ class BetterReflectionProvider implements ReflectionProvider /** @var ClassReflection[] */ private static array $anonymousClasses = []; - /** @var array */ + /** @var array */ private array $cachedConstants = []; /** - * @param string[] $universalObjectCratesClasses + * @param list $universalObjectCratesClasses */ public function __construct( 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, @@ -90,6 +100,8 @@ public function __construct( private FileHelper $fileHelper, private PhpStormStubsSourceStubber $phpstormStubsSourceStubber, private SignatureMapProvider $signatureMapProvider, + private AttributeReflectionFactory $attributeReflectionFactory, + #[AutowiredParameter(ref: '%universalObjectCratesClasses%')] private array $universalObjectCratesClasses, ) { @@ -143,6 +155,8 @@ public function getClass(string $className): ClassReflection $this->phpDocInheritanceResolver, $this->phpVersion, $this->signatureMapProvider, + $this->deprecationProvider, + $this->attributeReflectionFactory, $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(), @@ -176,11 +190,6 @@ public function getClassName(string $className): string return $reflectionClass->getName(); } - public function supportsAnonymousClasses(): bool - { - return true; - } - public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection { if (isset($classNode->namespacedName)) { @@ -202,6 +211,7 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $ $scopeFile, ); $classNode->name = new Node\Identifier($className); + $classNode->namespacedName = null; if (isset(self::$anonymousClasses[$className])) { return self::$anonymousClasses[$className]; @@ -214,12 +224,23 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $ 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('class@anonymous/%s:%s', $filename, $classNode->getStartLine()); + $displayName = sprintf('%s@anonymous/%s:%s', $displayParentName, $filename, $classNode->getStartLine()); } else { - $displayName = sprintf('class@anonymous/%s:%s:%d', $filename, $classNode->getStartLine(), $classLineIndex); + $displayName = sprintf('%s@anonymous/%s:%s:%d', $displayParentName, $filename, $classNode->getStartLine(), $classLineIndex); } self::$anonymousClasses[$className] = new ClassReflection( @@ -230,6 +251,8 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $ $this->phpDocInheritanceResolver, $this->phpVersion, $this->signatureMapProvider, + $this->deprecationProvider, + $this->attributeReflectionFactory, $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(), @@ -247,6 +270,11 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $ return self::$anonymousClasses[$className]; } + public function getUniversalObjectCratesClasses(): array + { + return $this->universalObjectCratesClasses; + } + public function hasFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool { return $this->resolveFunctionName($nameNode, $namespaceAnswerer) !== null; @@ -264,6 +292,10 @@ public function getFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAn 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; @@ -282,12 +314,15 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $phpDocParameterTypes = []; $phpDocReturnTag = null; $phpDocThrowsTag = null; - $deprecatedTag = null; - $isDeprecated = false; + + $deprecation = $this->deprecationProvider->getFunctionDeprecation($reflectionFunction); + $deprecationDescription = $deprecation === null ? null : $deprecation->getDescription(); + $isDeprecated = $deprecation !== null; + $isInternal = false; - $isFinal = false; $isPure = null; $asserts = Assertions::createEmpty(); + $acceptsNamedArguments = true; $phpDocComment = null; $phpDocParameterOutTags = []; $phpDocParameterImmediatelyInvokedCallable = []; @@ -304,15 +339,17 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $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(); @@ -324,22 +361,28 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $phpDocParameterTypes, $phpDocReturnTag !== null ? $phpDocReturnTag->getType() : null, $phpDocThrowsTag !== null ? $phpDocThrowsTag->getType() : null, - $deprecatedTag !== null ? $deprecatedTag->getMessage() : null, + $deprecationDescription, $isDeprecated, $isInternal, - $isFinal, $reflectionFunction->getFileName() !== false ? $reflectionFunction->getFileName() : null, $isPure, $asserts, + $acceptsNamedArguments, $phpDocComment, array_map(static fn (ParamOutTag $paramOutTag): Type => $paramOutTag->getType(), $phpDocParameterOutTags), $phpDocParameterImmediatelyInvokedCallable, array_map(static fn (ParamClosureThisTag $tag): Type => $tag->getType(), $phpDocParameterClosureThisTypeTags), + $this->attributeReflectionFactory->fromNativeReflection($reflectionFunction->getAttributes(), InitializerExprContext::fromFunction($reflectionFunction->getName(), $reflectionFunction->getFileName() !== false ? $reflectionFunction->getFileName() : null)), ); } public function resolveFunctionName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string { + $name = $nameNode->toLowerString(); + if (in_array($name, ['exit', 'die'], true)) { + return $name; + } + return $this->resolveName($nameNode, function (string $name): bool { try { $this->reflector->reflectFunction($name); @@ -362,7 +405,7 @@ public function hasConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAn return $this->resolveConstantName($nameNode, $namespaceAnswerer) !== null; } - public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): GlobalConstantReflection + public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ConstantReflection { $constantName = $this->resolveConstantName($nameNode, $namespaceAnswerer); if ($constantName === null) { @@ -378,13 +421,15 @@ public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAn $constantValueType = $this->initializerExprTypeResolver->getType($constantReflection->getValueExpression(), InitializerExprContext::fromGlobalConstant($constantReflection)); $docComment = $constantReflection->getDocComment(); - $isDeprecated = TrinaryLogic::createNo(); - $deprecatedDescription = null; - if ($docComment !== null) { + $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 = TrinaryLogic::createFromBoolean($resolvedPhpDoc->isDeprecated()); + $isDeprecated = $resolvedPhpDoc->isDeprecated(); - if ($resolvedPhpDoc->isDeprecated() && $resolvedPhpDoc->getDeprecatedTag() !== null) { + if ($isDeprecated && $resolvedPhpDoc->getDeprecatedTag() !== null) { $deprecatedMessage = $resolvedPhpDoc->getDeprecatedTag()->getMessage(); $matches = Strings::match($deprecatedMessage ?? '', '#^(\d+)\.(\d+)(?:\.(\d+))?$#'); @@ -394,7 +439,7 @@ public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAn $patch = $matches[3] ?? 0; $versionId = sprintf('%d%02d%02d', $major, $minor, $patch); - $isDeprecated = TrinaryLogic::createFromBoolean($this->phpVersion->getVersionId() >= $versionId); + $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 @@ -407,7 +452,7 @@ public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAn $constantName, $constantValueType, $fileName, - $isDeprecated, + TrinaryLogic::createFromBoolean($isDeprecated), $deprecatedDescription, ); } @@ -420,6 +465,8 @@ public function resolveConstantName(Node\Name $nameNode, ?NamespaceAnswerer $nam return true; } catch (IdentifierNotFound) { // pass + } catch (InvalidIdentifierName) { + // pass } catch (UnableToCompileNode) { // pass } diff --git a/src/Reflection/BetterReflection/BetterReflectionProviderFactory.php b/src/Reflection/BetterReflection/BetterReflectionProviderFactory.php deleted file mode 100644 index b6fa9c0309..0000000000 --- a/src/Reflection/BetterReflection/BetterReflectionProviderFactory.php +++ /dev/null @@ -1,14 +0,0 @@ -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( @@ -112,11 +137,16 @@ public function create(): SourceLocator 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([ - 'PHPStan\\Testing\\' => [$pharProtocolPath . '/src/Testing/'], - 'PHPStan\\BetterReflection\\' => [$pharProtocolPath . '/vendor/ondrejmirtes/better-reflection/src/'], - ]), + Psr4Mapping::fromArrayMappings($mappings), ); } } diff --git a/src/Reflection/BetterReflection/Reflector/MemoizingReflector.php b/src/Reflection/BetterReflection/Reflector/MemoizingReflector.php index 486ad45c20..84d858e6b3 100644 --- a/src/Reflection/BetterReflection/Reflector/MemoizingReflector.php +++ b/src/Reflection/BetterReflection/Reflector/MemoizingReflector.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\BetterReflection\Reflector; +use Override; use PHPStan\BetterReflection\Identifier\Identifier; use PHPStan\BetterReflection\Identifier\IdentifierType; use PHPStan\BetterReflection\Reflection\ReflectionClass; @@ -9,9 +10,12 @@ use PHPStan\BetterReflection\Reflection\ReflectionFunction; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; use PHPStan\BetterReflection\Reflector\Reflector; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use function array_key_exists; use function strtolower; +#[AutowiredService(name: 'betterReflectionReflector', as: Reflector::class)] final class MemoizingReflector implements Reflector { @@ -24,10 +28,14 @@ final class MemoizingReflector implements Reflector /** @var array */ private array $functionReflections = []; - public function __construct(private Reflector $reflector) + public function __construct( + #[AutowiredParameter(ref: '@originalBetterReflectionReflector')] + private Reflector $reflector, + ) { } + #[Override] public function reflectClass(string $className): ReflectionClass { $lowerClassName = strtolower($className); @@ -52,6 +60,7 @@ public function reflectClass(string $className): ReflectionClass } } + #[Override] public function reflectConstant(string $constantName): ReflectionConstant { if (array_key_exists($constantName, $this->constantReflections)) { @@ -72,6 +81,7 @@ public function reflectConstant(string $constantName): ReflectionConstant } } + #[Override] public function reflectFunction(string $functionName): ReflectionFunction { $lowerFunctionName = strtolower($functionName); @@ -93,16 +103,19 @@ public function reflectFunction(string $functionName): ReflectionFunction } } + #[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 index 2acb010ad7..3da44c64bf 100644 --- a/src/Reflection/BetterReflection/SourceLocator/AutoloadFunctionsSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/AutoloadFunctionsSourceLocator.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; @@ -12,7 +13,7 @@ use function PHPStan\autoloadFunctions; use function trait_exists; -class AutoloadFunctionsSourceLocator implements SourceLocator +final class AutoloadFunctionsSourceLocator implements SourceLocator { public function __construct( @@ -22,6 +23,7 @@ public function __construct( { } + #[Override] public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { if (!$identifier->isClass()) { @@ -50,6 +52,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): 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 969d978333..3c43f17c7c 100644 --- a/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use Override; use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; @@ -47,7 +48,7 @@ * * Modified code from Roave/BetterReflection, Copyright (c) 2017 Roave, LLC. */ -class AutoloadSourceLocator implements SourceLocator +final class AutoloadSourceLocator implements SourceLocator { /** @var array{classes: array, functions: array, constants: array} */ @@ -67,6 +68,7 @@ public function __construct(private FileNodesFetcher $fileNodesFetcher, private { } + #[Override] public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { if ($identifier->isFunction()) { @@ -278,6 +280,7 @@ private function findReflection(Reflector $reflector, string $file, Identifier $ return null; } + #[Override] public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array { return []; diff --git a/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php b/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php index 7e8367df3c..91f5fa1b6f 100644 --- a/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php +++ b/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php @@ -2,9 +2,10 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use Override; use PhpParser\Node; use PhpParser\Node\Stmt\Namespace_; -use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor; use PhpParser\NodeVisitorAbstract; use PHPStan\BetterReflection\Reflection\Exception\InvalidConstantNode; use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; @@ -12,7 +13,7 @@ use PHPStan\Reflection\ConstantNameHelper; use function strtolower; -class CachingVisitor extends NodeVisitorAbstract +final class CachingVisitor extends NodeVisitorAbstract { private string $fileName; @@ -30,6 +31,7 @@ class CachingVisitor extends NodeVisitorAbstract private ?Node\Stmt\Namespace_ $currentNamespaceNode = null; + #[Override] public function enterNode(Node $node): ?int { if ($node instanceof Namespace_) { @@ -51,7 +53,7 @@ public function enterNode(Node $node): ?int ); } - return NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CHILDREN; } if ($node instanceof Node\Stmt\Function_) { @@ -64,7 +66,7 @@ public function enterNode(Node $node): ?int ); } - return NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CHILDREN; } if ($node instanceof Node\Stmt\Const_) { @@ -80,7 +82,7 @@ public function enterNode(Node $node): ?int ); } - return NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CHILDREN; } if ($node instanceof Node\Expr\FuncCall) { @@ -101,7 +103,7 @@ public function enterNode(Node $node): ?int ); $this->constantNodes[ConstantNameHelper::normalize($constantName)][] = $constantNode; - return NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CHILDREN; } return null; @@ -110,6 +112,7 @@ public function enterNode(Node $node): ?int /** * @return null */ + #[Override] public function leaveNode(Node $node) { if (!$node instanceof Namespace_) { diff --git a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php index 3d123ccfe2..1195311fd2 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php +++ b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php @@ -8,6 +8,7 @@ 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; @@ -25,7 +26,8 @@ use function str_contains; use const GLOB_ONLYDIR; -class ComposerJsonAndInstalledJsonSourceLocatorMaker +#[AutowiredService] +final class ComposerJsonAndInstalledJsonSourceLocatorMaker { public function __construct( diff --git a/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php b/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php index 89e33adb3d..70eaadffbe 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php +++ b/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php @@ -8,7 +8,7 @@ /** * @template-covariant T of Node */ -class FetchedNode +final class FetchedNode { /** diff --git a/src/Reflection/BetterReflection/SourceLocator/FetchedNodesResult.php b/src/Reflection/BetterReflection/SourceLocator/FetchedNodesResult.php index aa98107ae3..ac90178d49 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FetchedNodesResult.php +++ b/src/Reflection/BetterReflection/SourceLocator/FetchedNodesResult.php @@ -4,7 +4,7 @@ use PhpParser\Node; -class FetchedNodesResult +final class FetchedNodesResult { /** diff --git a/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php b/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php index 0fa3440e29..1258607a9f 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php +++ b/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php @@ -3,15 +3,19 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; use PhpParser\NodeTraverser; +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 { public function __construct( private CachingVisitor $cachingVisitor, + #[AutowiredParameter(ref: '@defaultAnalysisParser')] private Parser $parser, ) { diff --git a/src/Reflection/BetterReflection/SourceLocator/NewOptimizedDirectorySourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/NewOptimizedDirectorySourceLocator.php deleted file mode 100644 index 557ac4848c..0000000000 --- a/src/Reflection/BetterReflection/SourceLocator/NewOptimizedDirectorySourceLocator.php +++ /dev/null @@ -1,217 +0,0 @@ - $classToFile - * @param array> $functionToFiles - * @param array $constantToFile - */ - public function __construct( - private FileNodesFetcher $fileNodesFetcher, - private array $classToFile, - private array $functionToFiles, - private array $constantToFile, - ) - { - } - - public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection - { - if ($identifier->isClass()) { - $className = strtolower($identifier->getName()); - $file = $this->findFileByClass($className); - if ($file === null) { - return null; - } - - $fetchedClassNodes = $this->fileNodesFetcher->fetchNodes($file)->getClassNodes(); - - if (!array_key_exists($className, $fetchedClassNodes)) { - return null; - } - - /** @var FetchedNode $fetchedClassNode */ - $fetchedClassNode = current($fetchedClassNodes[$className]); - - return $this->nodeToReflection($reflector, $fetchedClassNode); - } - - if ($identifier->isFunction()) { - $functionName = strtolower($identifier->getName()); - $files = $this->findFilesByFunction($functionName); - - $fetchedFunctionNode = null; - foreach ($files as $file) { - $fetchedFunctionNodes = $this->fileNodesFetcher->fetchNodes($file)->getFunctionNodes(); - - if (!array_key_exists($functionName, $fetchedFunctionNodes)) { - continue; - } - - /** @var FetchedNode $fetchedFunctionNode */ - $fetchedFunctionNode = current($fetchedFunctionNodes[$functionName]); - } - - if ($fetchedFunctionNode === null) { - return null; - } - - return $this->nodeToReflection($reflector, $fetchedFunctionNode); - } - - if ($identifier->isConstant()) { - $constantName = ConstantNameHelper::normalize($identifier->getName()); - $file = $this->findFileByConstant($constantName); - - if ($file === null) { - return null; - } - - $fetchedConstantNodes = $this->fileNodesFetcher->fetchNodes($file)->getConstantNodes(); - - if (!array_key_exists($constantName, $fetchedConstantNodes)) { - return null; - } - - /** @var FetchedNode $fetchedConstantNode */ - $fetchedConstantNode = current($fetchedConstantNodes[$constantName]); - - return $this->nodeToReflection( - $reflector, - $fetchedConstantNode, - $this->findConstantPositionInConstNode($fetchedConstantNode->getNode(), $constantName), - ); - } - - return null; - } - - /** - * @param FetchedNode|FetchedNode|FetchedNode $fetchedNode - */ - private function nodeToReflection(Reflector $reflector, FetchedNode $fetchedNode, ?int $positionInNode = null): Reflection - { - $nodeToReflection = new NodeToReflection(); - return $nodeToReflection->__invoke( - $reflector, - $fetchedNode->getNode(), - $fetchedNode->getLocatedSource(), - $fetchedNode->getNamespace(), - $positionInNode, - ); - } - - private function findFileByClass(string $className): ?string - { - if (!array_key_exists($className, $this->classToFile)) { - return null; - } - - return $this->classToFile[$className]; - } - - private function findFileByConstant(string $constantName): ?string - { - if (!array_key_exists($constantName, $this->constantToFile)) { - return null; - } - - return $this->constantToFile[$constantName]; - } - - /** - * @return string[] - */ - private function findFilesByFunction(string $functionName): array - { - if (!array_key_exists($functionName, $this->functionToFiles)) { - return []; - } - - return $this->functionToFiles[$functionName]; - } - - /** - * @return list - */ - public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array - { - $reflections = []; - if ($identifierType->isClass()) { - foreach ($this->classToFile as $file) { - $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); - foreach ($fetchedNodesResult->getClassNodes() as $identifierName => $fetchedClassNodes) { - foreach ($fetchedClassNodes as $fetchedClassNode) { - $reflections[$identifierName] = $this->nodeToReflection($reflector, $fetchedClassNode); - } - } - } - } elseif ($identifierType->isFunction()) { - foreach ($this->functionToFiles as $files) { - foreach ($files as $file) { - $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); - foreach ($fetchedNodesResult->getFunctionNodes() as $identifierName => $fetchedFunctionNodes) { - foreach ($fetchedFunctionNodes as $fetchedFunctionNode) { - $reflections[$identifierName] = $this->nodeToReflection($reflector, $fetchedFunctionNode); - continue 2; - } - } - } - } - } elseif ($identifierType->isConstant()) { - foreach ($this->constantToFile as $file) { - $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); - foreach ($fetchedNodesResult->getConstantNodes() as $identifierName => $fetchedConstantNodes) { - foreach ($fetchedConstantNodes as $fetchedConstantNode) { - $reflections[$identifierName] = $this->nodeToReflection( - $reflector, - $fetchedConstantNode, - $this->findConstantPositionInConstNode($fetchedConstantNode->getNode(), $identifierName), - ); - } - } - } - } - - return array_values($reflections); - } - - private function findConstantPositionInConstNode(Node\Stmt\Const_|Node\Expr\FuncCall $constantNode, string $constantName): ?int - { - if ($constantNode instanceof Node\Expr\FuncCall) { - return null; - } - - /** @var int $position */ - foreach ($constantNode->consts as $position => $const) { - if ($const->namespacedName === null) { - throw new ShouldNotHappenException(); - } - - if (ConstantNameHelper::normalize($const->namespacedName->toString()) === $constantName) { - return $position; - } - } - - throw new ShouldNotHappenException(); - } - -} diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php index b6fc6e471c..73d23ac817 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use Override; use PhpParser\Node; use PHPStan\BetterReflection\Identifier\Identifier; use PHPStan\BetterReflection\Identifier\IdentifierType; @@ -9,54 +10,31 @@ use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ConstantNameHelper; use PHPStan\ShouldNotHappenException; use function array_key_exists; use function array_values; -use function count; use function current; -use function in_array; -use function ltrim; -use function php_strip_whitespace; -use function preg_match_all; -use function preg_replace; -use function sprintf; use function strtolower; -/** - * @deprecated Use NewOptimizedDirectorySourceLocator - */ -class OptimizedDirectorySourceLocator implements SourceLocator +final class OptimizedDirectorySourceLocator implements SourceLocator { - private PhpFileCleaner $cleaner; - - private string $extraTypes; - - /** @var array|null */ - private ?array $classToFile = null; - - /** @var array|null */ - private ?array $constantToFile = null; - - /** @var array>|null */ - private ?array $functionToFiles = null; - /** - * @param string[] $files + * @param array $classToFile + * @param array> $functionToFiles + * @param array $constantToFile */ public function __construct( private FileNodesFetcher $fileNodesFetcher, - private PhpVersion $phpVersion, - private array $files, + private array $classToFile, + private array $functionToFiles, + private array $constantToFile, ) { - $this->extraTypes = $this->phpVersion->supportsEnums() ? '|enum' : ''; - - $this->cleaner = new PhpFileCleaner(); } + #[Override] public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { if ($identifier->isClass()) { @@ -145,13 +123,6 @@ private function nodeToReflection(Reflector $reflector, FetchedNode $fetchedNode private function findFileByClass(string $className): ?string { - if ($this->classToFile === null) { - $this->init(); - if ($this->classToFile === null) { - throw new ShouldNotHappenException(); - } - } - if (!array_key_exists($className, $this->classToFile)) { return null; } @@ -161,13 +132,6 @@ private function findFileByClass(string $className): ?string private function findFileByConstant(string $constantName): ?string { - if ($this->constantToFile === null) { - $this->init(); - if ($this->constantToFile === null) { - throw new ShouldNotHappenException(); - } - } - if (!array_key_exists($constantName, $this->constantToFile)) { return null; } @@ -180,13 +144,6 @@ private function findFileByConstant(string $constantName): ?string */ private function findFilesByFunction(string $functionName): array { - if ($this->functionToFiles === null) { - $this->init(); - if ($this->functionToFiles === null) { - throw new ShouldNotHappenException(); - } - } - if (!array_key_exists($functionName, $this->functionToFiles)) { return []; } @@ -194,118 +151,12 @@ private function findFilesByFunction(string $functionName): array return $this->functionToFiles[$functionName]; } - private function init(): void - { - $classToFile = []; - $constantToFile = []; - $functionToFiles = []; - foreach ($this->files as $file) { - $symbols = $this->findSymbols($file); - foreach ($symbols['classes'] as $classInFile) { - $classToFile[$classInFile] = $file; - } - foreach ($symbols['constants'] as $constantInFile) { - $constantToFile[$constantInFile] = $file; - } - foreach ($symbols['functions'] as $functionInFile) { - if (!array_key_exists($functionInFile, $functionToFiles)) { - $functionToFiles[$functionInFile] = []; - } - $functionToFiles[$functionInFile][] = $file; - } - } - - $this->classToFile = $classToFile; - $this->functionToFiles = $functionToFiles; - $this->constantToFile = $constantToFile; - } - - /** - * Inspired by Composer\Autoload\ClassMapGenerator::findClasses() - * @link https://github.com/composer/composer/blob/45d3e133a4691eccb12e9cd6f9dfd76eddc1906d/src/Composer/Autoload/ClassMapGenerator.php#L216 - * - * @return array{classes: string[], functions: string[], constants: string[]} - */ - private function findSymbols(string $file): array - { - $contents = @php_strip_whitespace($file); - if ($contents === '') { - return ['classes' => [], 'functions' => [], 'constants' => []]; - } - - $matchResults = (bool) preg_match_all(sprintf('{\b(?:(?:class|interface|trait|const|function%s)\s)|(?:define\s*\()}i', $this->extraTypes), $contents, $matches); - if (!$matchResults) { - return ['classes' => [], 'functions' => [], 'constants' => []]; - } - - $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' => $classes, - 'functions' => $functions, - 'constants' => $constants, - ]; - } - /** * @return list */ + #[Override] public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array { - if ($this->classToFile === null || $this->functionToFiles === null || $this->constantToFile === null) { - $this->init(); - if ($this->classToFile === null || $this->functionToFiles === null || $this->constantToFile === null) { - throw new ShouldNotHappenException(); - } - } - $reflections = []; if ($identifierType->isClass()) { foreach ($this->classToFile as $file) { diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php index a147d1872e..5803517e63 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php @@ -3,6 +3,8 @@ 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; @@ -17,7 +19,8 @@ use function sprintf; use function strtolower; -class OptimizedDirectorySourceLocatorFactory +#[AutowiredService] +final class OptimizedDirectorySourceLocatorFactory { private PhpFileCleaner $cleaner; @@ -26,6 +29,7 @@ class OptimizedDirectorySourceLocatorFactory public function __construct( private FileNodesFetcher $fileNodesFetcher, + #[AutowiredParameter(ref: '@fileFinderScan')] private FileFinder $fileFinder, private PhpVersion $phpVersion, private Cache $cache, @@ -35,7 +39,7 @@ public function __construct( $this->cleaner = new PhpFileCleaner(); } - public function createByDirectory(string $directory): NewOptimizedDirectorySourceLocator + public function createByDirectory(string $directory): OptimizedDirectorySourceLocator { $files = $this->fileFinder->findFiles([$directory])->getFiles(); $fileHashes = []; @@ -79,7 +83,7 @@ public function createByDirectory(string $directory): NewOptimizedDirectorySourc [$classToFile, $functionToFiles, $constantToFile] = $this->changeStructure($cached); - return new NewOptimizedDirectorySourceLocator( + return new OptimizedDirectorySourceLocator( $this->fileNodesFetcher, $classToFile, $functionToFiles, @@ -90,7 +94,7 @@ public function createByDirectory(string $directory): NewOptimizedDirectorySourc /** * @param string[] $files */ - public function createByFiles(array $files): NewOptimizedDirectorySourceLocator + public function createByFiles(array $files): OptimizedDirectorySourceLocator { $symbols = []; foreach ($files as $file) { @@ -100,7 +104,7 @@ public function createByFiles(array $files): NewOptimizedDirectorySourceLocator [$classToFile, $functionToFiles, $constantToFile] = $this->changeStructure($symbols); - return new NewOptimizedDirectorySourceLocator( + return new OptimizedDirectorySourceLocator( $this->fileNodesFetcher, $classToFile, $functionToFiles, diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php index c636424c2f..e0404ad629 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php @@ -2,19 +2,21 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use PHPStan\DependencyInjection\AutowiredService; use function array_key_exists; -class OptimizedDirectorySourceLocatorRepository +#[AutowiredService] +final class OptimizedDirectorySourceLocatorRepository { - /** @var array */ + /** @var array */ private array $locators = []; public function __construct(private OptimizedDirectorySourceLocatorFactory $factory) { } - public function getOrCreate(string $directory): NewOptimizedDirectorySourceLocator + public function getOrCreate(string $directory): OptimizedDirectorySourceLocator { if (array_key_exists($directory, $this->locators)) { return $this->locators[$directory]; diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php index 7abfec64da..e2768bc6d7 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php @@ -2,15 +2,18 @@ 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 { /** @var array */ @@ -23,6 +26,7 @@ public function __construct( { } + #[Override] public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { foreach ($this->locators as $locator) { @@ -56,6 +60,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): /** * @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 982a372d4a..b75b4c7f66 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use Override; use PhpParser\Node\Stmt\Const_; use PHPStan\BetterReflection\Identifier\Identifier; use PHPStan\BetterReflection\Identifier\IdentifierType; @@ -12,13 +13,15 @@ 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 { /** @var array{classes: array, functions: array, constants: array}|null */ @@ -31,6 +34,7 @@ public function __construct( { } + #[Override] public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { if ($this->presentSymbols !== null) { @@ -167,6 +171,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): throw new ShouldNotHappenException(); } + #[Override] public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array { $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($this->fileName); diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php index 59e26df126..b97c230f79 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php @@ -2,9 +2,11 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use PHPStan\DependencyInjection\AutowiredService; use function array_key_exists; -class OptimizedSingleFileSourceLocatorRepository +#[AutowiredService] +final class OptimizedSingleFileSourceLocatorRepository { /** @var array */ diff --git a/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php b/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php index 6e76066fee..137941d004 100644 --- a/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php +++ b/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php @@ -14,7 +14,7 @@ * @author Jordi Boggiano * @see https://github.com/composer/composer/pull/10107 */ -class PhpFileCleaner +final class PhpFileCleaner { /** @var array */ @@ -289,7 +289,7 @@ private function peek(string $char): bool */ private function match(string $regex, ?array &$match = null, ?int $offset = null): bool { - return preg_match($regex, $this->contents, $match, 0, $offset ?? $this->index) === 1; + 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 0731d733b0..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,7 +10,7 @@ use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; -class PhpVersionBlacklistSourceLocator implements SourceLocator +final class PhpVersionBlacklistSourceLocator implements SourceLocator { public function __construct( @@ -19,6 +20,7 @@ public function __construct( { } + #[Override] public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { if ($identifier->isClass()) { @@ -36,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 index 1e20425451..d35b677a1a 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ReflectionClassSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/ReflectionClassSourceLocator.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; @@ -12,7 +13,7 @@ use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; use ReflectionClass; -class ReflectionClassSourceLocator implements SourceLocator +final class ReflectionClassSourceLocator implements SourceLocator { public function __construct( @@ -22,6 +23,7 @@ public function __construct( { } + #[Override] public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { if (!$identifier->isClass()) { @@ -45,6 +47,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): ); } + #[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 index 3993684ddd..6be98f2ba5 100644 --- a/src/Reflection/BetterReflection/SourceLocator/RewriteClassAliasSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/RewriteClassAliasSourceLocator.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; @@ -12,13 +13,14 @@ use function interface_exists; use function trait_exists; -class RewriteClassAliasSourceLocator implements SourceLocator +final class RewriteClassAliasSourceLocator implements SourceLocator { public function __construct(private SourceLocator $originalSourceLocator) { } + #[Override] public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { if (!$identifier->isClass()) { @@ -38,6 +40,7 @@ class_exists($identifier->getName(), false) 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 fc8e931ffe..8e1da7dec0 100644 --- a/src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.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; @@ -10,13 +11,14 @@ use ReflectionClass; use function class_exists; -class SkipClassAliasSourceLocator implements SourceLocator +final class SkipClassAliasSourceLocator implements SourceLocator { public function __construct(private SourceLocator $sourceLocator) { } + #[Override] public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { if ($identifier->isClass()) { @@ -39,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/PhpStormStubsSourceStubberFactory.php b/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php index 3d5615b24f..4049733f0f 100644 --- a/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php +++ b/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php @@ -4,13 +4,21 @@ use PhpParser\Parser; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Printer\Printer; use PHPStan\Php\PhpVersion; -class PhpStormStubsSourceStubberFactory +#[AutowiredService] +final class PhpStormStubsSourceStubberFactory { - public function __construct(private Parser $phpParser, private Printer $printer, private PhpVersion $phpVersion) + public function __construct( + #[AutowiredParameter(ref: '@php8PhpParser')] + private Parser $phpParser, + private Printer $printer, + private PhpVersion $phpVersion, + ) { } diff --git a/src/Reflection/BetterReflection/SourceStubber/ReflectionSourceStubberFactory.php b/src/Reflection/BetterReflection/SourceStubber/ReflectionSourceStubberFactory.php index 04da7cd605..0a1f1e9e6a 100644 --- a/src/Reflection/BetterReflection/SourceStubber/ReflectionSourceStubberFactory.php +++ b/src/Reflection/BetterReflection/SourceStubber/ReflectionSourceStubberFactory.php @@ -3,10 +3,12 @@ namespace PHPStan\Reflection\BetterReflection\SourceStubber; use PHPStan\BetterReflection\SourceLocator\SourceStubber\ReflectionSourceStubber; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Printer\Printer; use PHPStan\Php\PhpVersion; -class ReflectionSourceStubberFactory +#[AutowiredService] +final class ReflectionSourceStubberFactory { public function __construct(private Printer $printer, private PhpVersion $phpVersion) 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); + return array_map(static fn (ExtendedParametersAcceptor $variant) => new self($function, $variant), $variants); } public function getTemplateTypeMap(): TemplateTypeMap @@ -52,7 +52,7 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap } /** - * @return array + * @return list */ public function getParameters(): array { @@ -163,4 +163,9 @@ public function getUsedVariables(): array return []; } + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->function->acceptsNamedArguments(); + } + } diff --git a/src/Reflection/Callables/SimpleImpurePoint.php b/src/Reflection/Callables/SimpleImpurePoint.php index 91b807b3ad..79a3c96761 100644 --- a/src/Reflection/Callables/SimpleImpurePoint.php +++ b/src/Reflection/Callables/SimpleImpurePoint.php @@ -11,7 +11,7 @@ /** * @phpstan-import-type ImpurePointIdentifier from ImpurePoint */ -class SimpleImpurePoint +final class SimpleImpurePoint { /** diff --git a/src/Reflection/Callables/SimpleThrowPoint.php b/src/Reflection/Callables/SimpleThrowPoint.php index 6a32d4eacd..5cde43a155 100644 --- a/src/Reflection/Callables/SimpleThrowPoint.php +++ b/src/Reflection/Callables/SimpleThrowPoint.php @@ -6,7 +6,7 @@ use PHPStan\Type\Type; use Throwable; -class SimpleThrowPoint +final class SimpleThrowPoint { private function __construct( diff --git a/src/Reflection/ClassConstantReflection.php b/src/Reflection/ClassConstantReflection.php index fd69ce8129..6d0452caff 100644 --- a/src/Reflection/ClassConstantReflection.php +++ b/src/Reflection/ClassConstantReflection.php @@ -3,154 +3,27 @@ namespace PHPStan\Reflection; use PhpParser\Node\Expr; -use PHPStan\BetterReflection\NodeCompiler\Exception\UnableToCompileNode; -use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClassConstant; -use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -use PHPStan\Type\TypehintHelper; -use const NAN; /** @api */ -class ClassConstantReflection implements ConstantReflection +interface ClassConstantReflection extends ClassMemberReflection, ConstantReflection { - private ?Type $valueType = null; + public function getValueExpr(): Expr; - 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, - ) - { - } + public function isFinal(): bool; - public function getName(): string - { - return $this->reflection->getName(); - } + public function hasPhpDocType(): bool; - public function getFileName(): ?string - { - return $this->declaringClass->getFileName(); - } + public function getPhpDocType(): ?Type; - /** - * @deprecated Use getValueExpr() - * @return mixed - */ - public function getValue() - { - try { - return $this->reflection->getValue(); - } catch (UnableToCompileNode) { - return NAN; - } - } - - 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 hasNativeType(): bool; - public function getDeclaringClass(): ClassReflection - { - return $this->declaringClass; - } + public function getNativeType(): ?Type; - 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->reflection->isFinal(); - } - - public function isDeprecated(): TrinaryLogic - { - return TrinaryLogic::createFromBoolean($this->isDeprecated); - } - - public function getDeprecatedDescription(): ?string - { - if ($this->isDeprecated) { - return $this->deprecatedDescription; - } - - return null; - } - - public function isInternal(): TrinaryLogic - { - return TrinaryLogic::createFromBoolean($this->isInternal); - } - - public function getDocComment(): ?string - { - $docComment = $this->reflection->getDocComment(); - if ($docComment === false) { - return null; - } - - return $docComment; - } + /** + * @return list + */ + public function getAttributes(): array; } diff --git a/src/Reflection/ClassMemberAccessAnswerer.php b/src/Reflection/ClassMemberAccessAnswerer.php index ed1803e786..9eeb979821 100644 --- a/src/Reflection/ClassMemberAccessAnswerer.php +++ b/src/Reflection/ClassMemberAccessAnswerer.php @@ -13,10 +13,17 @@ public function isInClass(): bool; public function getClassReflection(): ?ClassReflection; + /** + * @deprecated Use canReadProperty() or canWriteProperty() + */ public function canAccessProperty(PropertyReflection $propertyReflection): bool; + public function canReadProperty(ExtendedPropertyReflection $propertyReflection): bool; + + public function canWriteProperty(ExtendedPropertyReflection $propertyReflection): bool; + public function canCallMethod(MethodReflection $methodReflection): bool; - public function canAccessConstant(ConstantReflection $constantReflection): bool; + public function canAccessConstant(ClassConstantReflection $constantReflection): bool; } diff --git a/src/Reflection/ClassNameHelper.php b/src/Reflection/ClassNameHelper.php index adf7905bfc..815534f310 100644 --- a/src/Reflection/ClassNameHelper.php +++ b/src/Reflection/ClassNameHelper.php @@ -3,15 +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 7c9a569dcc..05a8b29248 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -23,9 +23,11 @@ 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; @@ -69,17 +71,19 @@ use function sprintf; use function strtolower; -/** @api */ -class ClassReflection +/** + * @api + */ +final class ClassReflection { /** @var ExtendedMethodReflection[] */ private array $methods = []; - /** @var PropertyReflection[] */ + /** @var ExtendedPropertyReflection[] */ private array $properties = []; - /** @var ClassConstantReflection[] */ + /** @var RealClassClassConstantReflection[] */ private array $constants = []; /** @var EnumCaseReflection[]|null */ @@ -126,7 +130,9 @@ class ClassReflection private false|ResolvedPhpDocBlock $resolvedPhpDocBlock = false; - /** @var ClassReflection[]|null */ + private false|ResolvedPhpDocBlock $traitContextResolvedPhpDocBlock = false; + + /** @var array|null */ private ?array $cachedInterfaces = null; private ClassReflection|false|null $cachedParentClass = false; @@ -137,6 +143,12 @@ class ClassReflection /** @var array */ private static array $resolvingTypeAliasImports = []; + /** @var array */ + private array $hasMethodCache = []; + + /** @var array */ + private array $hasPropertyCache = []; + /** * @param PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions * @param MethodsClassReflectionExtension[] $methodsClassReflectionExtensions @@ -151,6 +163,8 @@ public function __construct( private PhpDocInheritanceResolver $phpDocInheritanceResolver, private PhpVersion $phpVersion, private SignatureMapProvider $signatureMapProvider, + private DeprecationProvider $deprecationProvider, + private AttributeReflectionFactory $attributeReflectionFactory, private array $propertiesClassReflectionExtensions, private array $methodsClassReflectionExtensions, private array $allowedSubTypesClassReflectionExtensions, @@ -164,6 +178,7 @@ public function __construct( private array $universalObjectCratesClasses, private ?string $extraCacheKey = null, private ?TemplateTypeVarianceMap $resolvedCallSiteVarianceMap = null, + private ?bool $finalByKeywordOverride = null, ) { } @@ -194,14 +209,6 @@ public function getFileName(): ?string return $this->filename = $fileName; } - /** - * @deprecated Use getFileName() - */ - public function getFileNameWithPhpDocs(): ?string - { - return $this->getFileName(); - } - public function getParentClass(): ?ClassReflection { if (!is_bool($this->cachedParentClass)) { @@ -303,6 +310,10 @@ public function getCacheKey(): string $cacheKey .= '<' . implode(',', $templateTypes) . '>'; } + if ($this->hasFinalByKeywordOverride()) { + $cacheKey .= '-f=' . ($this->isFinalByKeyword() ? 't' : 'f'); + } + if ($this->extraCacheKey !== null) { $cacheKey .= '-' . $this->extraCacheKey; } @@ -364,7 +375,7 @@ public function getClassHierarchyDistances(): array } /** - * @return ReflectionClass[] + * @return list */ private function collectTraits(ReflectionClass|ReflectionEnum $class): array { @@ -405,7 +416,6 @@ public function allowsDynamicProperties(): bool if (UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( $this->reflectionProvider, - $this->universalObjectCratesClasses, $this, )) { return true; @@ -455,8 +465,12 @@ private function allowsDynamicPropertiesExtensions(): bool public function hasProperty(string $propertyName): bool { + if (array_key_exists($propertyName, $this->hasPropertyCache)) { + return $this->hasPropertyCache[$propertyName]; + } + if ($this->isEnum()) { - return $this->hasNativeProperty($propertyName); + return $this->hasPropertyCache[$propertyName] = $this->hasNativeProperty($propertyName); } foreach ($this->propertiesClassReflectionExtensions as $i => $extension) { @@ -464,30 +478,34 @@ public function hasProperty(string $propertyName): bool break; } if ($extension->hasProperty($this, $propertyName)) { - return true; + return $this->hasPropertyCache[$propertyName] = true; } } if ($this->requireExtendsPropertiesClassReflectionExtension->hasProperty($this, $propertyName)) { - return true; + return $this->hasPropertyCache[$propertyName] = true; } - return false; + return $this->hasPropertyCache[$propertyName] = false; } public function hasMethod(string $methodName): bool { + if (array_key_exists($methodName, $this->hasMethodCache)) { + return $this->hasMethodCache[$methodName]; + } + foreach ($this->methodsClassReflectionExtensions as $extension) { if ($extension->hasMethod($this, $methodName)) { - return true; + return $this->hasMethodCache[$methodName] = true; } } if ($this->requireExtendsMethodsClassReflectionExtension->hasMethod($this, $methodName)) { - return true; + return $this->hasMethodCache[$methodName] = true; } - return false; + return $this->hasMethodCache[$methodName] = false; } public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection @@ -534,6 +552,15 @@ private function wrapExtendedMethod(MethodReflection $method): ExtendedMethodRef 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); @@ -616,7 +643,7 @@ public function evictPrivateSymbols(): void $this->getPhpExtension()->evictPrivateSymbols($this->getCacheKey()); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { if ($this->isEnum()) { return $this->getNativeProperty($propertyName); @@ -637,8 +664,8 @@ public function getProperty(string $propertyName, ClassMemberAccessAnswerer $sco continue; } - $property = $extension->getProperty($this, $propertyName); - if ($scope->canAccessProperty($property)) { + $property = $this->wrapExtendedProperty($propertyName, $extension->getProperty($this, $propertyName)); + if ($scope->canReadProperty($property)) { return $this->properties[$key] = $property; } $this->properties[$key] = $property; @@ -690,6 +717,7 @@ public function isTrait(): bool /** * @phpstan-assert-if-true ReflectionEnum $this->reflection + * @phpstan-assert-if-true ReflectionEnum $this->getNativeReflection() */ public function isEnum(): bool { @@ -768,9 +796,9 @@ public function getEnumCases(): array if ($case instanceof ReflectionEnumBackedCase) { $valueType = $this->initializerExprTypeResolver->getType($case->getValueExpression(), $initializerExprContext); } - /** @var string $caseName */ $caseName = $case->getName(); - $cases[$caseName] = new EnumCaseReflection($this, $caseName, $valueType); + $attributes = $this->attributeReflectionFactory->fromNativeReflection($case->getAttributes(), InitializerExprContext::fromClass($this->getName(), $this->getFileName())); + $cases[$caseName] = new EnumCaseReflection($this, $case, $valueType, $attributes, $this->deprecationProvider); } return $this->enumCases = $cases; @@ -796,7 +824,9 @@ public function getEnumCase(string $name): EnumCaseReflection $valueType = $this->initializerExprTypeResolver->getType($case->getValueExpression(), InitializerExprContext::fromClassReflection($this)); } - return new EnumCaseReflection($this, $name, $valueType); + $attributes = $this->attributeReflectionFactory->fromNativeReflection($case->getAttributes(), InitializerExprContext::fromClass($this->getName(), $this->getFileName())); + + return new EnumCaseReflection($this, $case, $valueType, $attributes, $this->deprecationProvider); } public function isClass(): bool @@ -814,20 +844,33 @@ public function is(string $className): bool return $this->getName() === $className || $this->isSubclassOf($className); } + /** + * @deprecated Use isSubclassOfClass instead. + */ public function isSubclassOf(string $className): bool { - if (isset($this->subclasses[$className])) { - return $this->subclasses[$className]; + if (!$this->reflectionProvider->hasClass($className)) { + return false; } - if (!$this->reflectionProvider->hasClass($className)) { - return $this->subclasses[$className] = false; + return $this->isSubclassOfClass($this->reflectionProvider->getClass($className)); + } + + public function isSubclassOfClass(self $class): bool + { + $cacheKey = $class->getCacheKey(); + if (isset($this->subclasses[$cacheKey])) { + return $this->subclasses[$cacheKey]; + } + + if ($class->isFinalByKeyword() || $class->isAnonymous()) { + return $this->subclasses[$cacheKey] = false; } try { - return $this->subclasses[$className] = $this->reflection->isSubclassOf($className); + return $this->subclasses[$cacheKey] = $this->reflection->isSubclassOf($class->getName()); } catch (ReflectionException) { - return $this->subclasses[$className] = false; + return $this->subclasses[$cacheKey] = false; } } @@ -841,7 +884,7 @@ public function implementsInterface(string $className): bool } /** - * @return ClassReflection[] + * @return list */ public function getParents(): array { @@ -856,7 +899,7 @@ public function getParents(): array } /** - * @return ClassReflection[] + * @return array */ public function getInterfaces(): array { @@ -890,7 +933,7 @@ public function getInterfaces(): array } /** - * @return ClassReflection[] + * @return array */ private function collectInterfaces(ClassReflection $interface): array { @@ -906,7 +949,7 @@ private function collectInterfaces(ClassReflection $interface): array } /** - * @return ClassReflection[] + * @return array */ public function getImmediateInterfaces(): array { @@ -1043,6 +1086,10 @@ public function getConstant(string $name): ClassConstantReflection throw new MissingConstantFromReflectionException($this->getName(), $name); } + $deprecation = $this->deprecationProvider->getClassConstantDeprecation($reflectionConstant); + $deprecatedDescription = $deprecation === null ? null : $deprecation->getDescription(); + $isDeprecated = $deprecation !== null; + $declaringClass = $this->reflectionProvider->getClass($reflectionConstant->getDeclaringClass()->getName()); $fileName = $declaringClass->getFileName(); $phpDocType = null; @@ -1063,22 +1110,29 @@ public function getConstant(string $name): ClassConstantReflection ); } - $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(); + if (!$isDeprecated) { + $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; + $isDeprecated = $resolvedPhpDoc->isDeprecated(); } + $isInternal = $resolvedPhpDoc->isInternal(); + $isFinal = $resolvedPhpDoc->isFinal(); $nativeType = null; if ($reflectionConstant->getType() !== null) { - $nativeType = TypehintHelper::decideTypeFromReflection($reflectionConstant->getType(), null, $declaringClass); + $nativeType = TypehintHelper::decideTypeFromReflection($reflectionConstant->getType(), selfClass: $declaringClass); } elseif ($this->signatureMapProvider->hasClassConstantMetadata($declaringClass->getName(), $name)) { $nativeType = $this->signatureMapProvider->getClassConstantMetadata($declaringClass->getName(), $name)['nativeType']; } - $this->constants[$name] = new ClassConstantReflection( + $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 RealClassClassConstantReflection( $this->initializerExprTypeResolver, $declaringClass, $reflectionConstant, @@ -1087,6 +1141,8 @@ public function getConstant(string $name): ClassConstantReflection $deprecatedDescription, $isDeprecated, $isInternal, + $isFinal, + $this->attributeReflectionFactory->fromNativeReflection($reflectionConstant->getAttributes(), InitializerExprContext::fromClass($declaringClass->getName(), $fileName)), ); } return $this->constants[$name]; @@ -1098,7 +1154,7 @@ public function hasTraitUse(string $traitName): bool } /** - * @return string[] + * @return list */ private function getTraitNames(): array { @@ -1171,11 +1227,8 @@ public function getTypeAliases(): array public function getDeprecatedDescription(): ?string { - if ($this->deprecatedDescription === null && $this->isDeprecated()) { - $resolvedPhpDoc = $this->getResolvedPhpDoc(); - if ($resolvedPhpDoc !== null && $resolvedPhpDoc->getDeprecatedTag() !== null) { - $this->deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag()->getMessage(); - } + if ($this->isDeprecated === null) { + $this->resolveDeprecation(); } return $this->deprecatedDescription; @@ -1184,13 +1237,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(); @@ -1224,7 +1300,7 @@ public function isImmutable(): bool { if ($this->isImmutable === null) { $resolvedPhpDoc = $this->getResolvedPhpDoc(); - $this->isImmutable = $resolvedPhpDoc !== null && $resolvedPhpDoc->isImmutable(); + $this->isImmutable = $resolvedPhpDoc !== null && ($resolvedPhpDoc->isImmutable() || $resolvedPhpDoc->isReadOnly()); $parentClass = $this->getParentClass(); if ($parentClass !== null && !$this->isImmutable) { @@ -1255,12 +1331,21 @@ public function acceptsNamedArguments(): bool 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(); } @@ -1284,14 +1369,17 @@ private function findAttributeFlags(): ?int $attributeClass = $this->reflectionProvider->getClass(Attribute::class); $arguments = []; foreach ($nativeAttributes[0]->getArgumentsExpressions() as $i => $expression) { - $arguments[] = new Arg($expression, false, false, [], is_int($i) ? null : new Identifier($i)); + if ($i === '') { + throw new ShouldNotHappenException(); + } + $arguments[] = new Arg($expression, name: is_int($i) ? null : new Identifier($i)); } if (!$attributeClass->hasConstructor()) { return null; } $attributeConstructor = $attributeClass->getConstructor(); - $attributeConstructorVariant = ParametersAcceptorSelector::selectSingle($attributeConstructor->getVariants()); + $attributeConstructorVariant = $attributeConstructor->getOnlyVariant(); if (count($arguments) === 0) { $flagType = $attributeConstructorVariant->getParameters()[0]->getDefaultValue(); @@ -1317,6 +1405,14 @@ private function findAttributeFlags(): ?int return null; } + /** + * @return list + */ + public function getAttributes(): array + { + return $this->attributeReflectionFactory->fromNativeReflection($this->reflection->getAttributes(), InitializerExprContext::fromClass($this->getName(), $this->getFileName())); + } + public function getAttributeClassFlags(): int { $flags = $this->findAttributeFlags(); @@ -1327,6 +1423,19 @@ public function getAttributeClassFlags(): int return $flags; } + public function getObjectType(): ObjectType + { + if (!$this->isGeneric()) { + return new ObjectType($this->getName()); + } + + return new GenericObjectType( + $this->getName(), + $this->typeMapToList($this->getActiveTemplateTypeMap()), + variances: $this->varianceMapToList($this->getCallSiteVarianceMap()), + ); + } + public function getTemplateTypeMap(): TemplateTypeMap { if ($this->templateTypeMap !== null) { @@ -1360,7 +1469,7 @@ public function getActiveTemplateTypeMap(): TemplateTypeMap if ($type instanceof ErrorType) { $templateType = $templateTypeMap->getType($name); if ($templateType !== null) { - return TemplateTypeHelper::resolveToBounds($templateType); + return TemplateTypeHelper::resolveToDefaults($templateType); } } @@ -1428,7 +1537,7 @@ public function typeMapFromList(array $types): TemplateTypeMap $map = []; $i = 0; foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { - $map[$tag->getName()] = $types[$i] ?? $tag->getBound(); + $map[$tag->getName()] = $types[$i] ?? $tag->getDefault() ?? $tag->getBound(); $i++; } @@ -1455,7 +1564,7 @@ public function varianceMapFromList(array $variances): TemplateTypeVarianceMap return new TemplateTypeVarianceMap($map); } - /** @return array */ + /** @return list */ public function typeMapToList(TemplateTypeMap $typeMap): array { $resolvedPhpDoc = $this->getResolvedPhpDoc(); @@ -1465,13 +1574,13 @@ 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 array */ + /** @return list */ public function varianceMapToList(TemplateTypeVarianceMap $varianceMap): array { $resolvedPhpDoc = $this->getResolvedPhpDoc(); @@ -1500,6 +1609,8 @@ public function withTypes(array $types): self $this->phpDocInheritanceResolver, $this->phpVersion, $this->signatureMapProvider, + $this->deprecationProvider, + $this->attributeReflectionFactory, $this->propertiesClassReflectionExtensions, $this->methodsClassReflectionExtensions, $this->allowedSubTypesClassReflectionExtensions, @@ -1513,6 +1624,7 @@ public function withTypes(array $types): self $this->universalObjectCratesClasses, null, $this->resolvedCallSiteVarianceMap, + $this->finalByKeywordOverride, ); } @@ -1529,6 +1641,8 @@ public function withVariances(array $variances): self $this->phpDocInheritanceResolver, $this->phpVersion, $this->signatureMapProvider, + $this->deprecationProvider, + $this->attributeReflectionFactory, $this->propertiesClassReflectionExtensions, $this->methodsClassReflectionExtensions, $this->allowedSubTypesClassReflectionExtensions, @@ -1542,6 +1656,49 @@ public function withVariances(array $variances): self $this->universalObjectCratesClasses, null, $this->varianceMapFromList($variances), + $this->finalByKeywordOverride, + ); + } + + public function asFinal(): self + { + if ($this->getNativeReflection()->isFinal()) { + return $this; + } + if ($this->finalByKeywordOverride === true) { + return $this; + } + if (!$this->isClass()) { + return $this; + } + if ($this->isAbstract()) { + return $this; + } + + return new self( + $this->reflectionProvider, + $this->initializerExprTypeResolver, + $this->fileTypeMapper, + $this->stubPhpDocProvider, + $this->phpDocInheritanceResolver, + $this->phpVersion, + $this->signatureMapProvider, + $this->deprecationProvider, + $this->attributeReflectionFactory, + $this->propertiesClassReflectionExtensions, + $this->methodsClassReflectionExtensions, + $this->allowedSubTypesClassReflectionExtensions, + $this->requireExtendsPropertiesClassReflectionExtension, + $this->requireExtendsMethodsClassReflectionExtension, + $this->displayName, + $this->reflection, + $this->anonymousFilename, + $this->resolvedTemplateTypeMap, + $this->stubPhpDocBlock, + $this->universalObjectCratesClasses, + null, + $this->resolvedCallSiteVarianceMap, + true, ); } @@ -1568,6 +1725,31 @@ public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock return $this->resolvedPhpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc($fileName, $this->getName(), null, null, $this->reflectionDocComment); } + public function getTraitContextResolvedPhpDoc(self $implementingClass): ?ResolvedPhpDocBlock + { + if (!$this->isTrait()) { + throw new ShouldNotHappenException(); + } + if ($implementingClass->isTrait()) { + throw new ShouldNotHappenException(); + } + $fileName = $this->getFileName(); + if (is_bool($this->reflectionDocComment)) { + $docComment = $this->reflection->getDocComment(); + $this->reflectionDocComment = $docComment !== false ? $docComment : null; + } + + if ($this->reflectionDocComment === null) { + return null; + } + + 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 { foreach ($this->getExtendsTags() as $tag) { @@ -1720,7 +1902,20 @@ public function getRequireImplementsTags(): array } /** - * @return array + * @return array + */ + public function getSealedTags(): array + { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + return []; + } + + return $resolvedPhpDoc->getSealedTags(); + } + + /** + * @return array */ public function getPropertyTags(): array { @@ -1733,7 +1928,7 @@ public function getPropertyTags(): array } /** - * @return array + * @return array */ public function getMethodTags(): array { @@ -1746,7 +1941,7 @@ public function getMethodTags(): array } /** - * @return array + * @return list */ public function getResolvedMixinTypes(): array { diff --git a/src/Reflection/ClassReflectionExtensionRegistry.php b/src/Reflection/ClassReflectionExtensionRegistry.php index 6ba9c4d28a..1705f47461 100644 --- a/src/Reflection/ClassReflectionExtensionRegistry.php +++ b/src/Reflection/ClassReflectionExtensionRegistry.php @@ -2,12 +2,10 @@ namespace PHPStan\Reflection; -use PHPStan\Broker\Broker; use PHPStan\Reflection\RequireExtension\RequireExtendsMethodsClassReflectionExtension; use PHPStan\Reflection\RequireExtension\RequireExtendsPropertiesClassReflectionExtension; -use function array_merge; -class ClassReflectionExtensionRegistry +final class ClassReflectionExtensionRegistry { /** @@ -16,7 +14,6 @@ class ClassReflectionExtensionRegistry * @param AllowedSubTypesClassReflectionExtension[] $allowedSubTypesClassReflectionExtensions */ public function __construct( - Broker $broker, private array $propertiesClassReflectionExtensions, private array $methodsClassReflectionExtensions, private array $allowedSubTypesClassReflectionExtensions, @@ -24,13 +21,6 @@ public function __construct( private RequireExtendsMethodsClassReflectionExtension $requireExtendsMethodsClassReflectionExtension, ) { - foreach (array_merge($propertiesClassReflectionExtensions, $methodsClassReflectionExtensions, $allowedSubTypesClassReflectionExtensions) as $extension) { - if (!($extension instanceof BrokerAwareExtension)) { - continue; - } - - $extension->setBroker($broker); - } } /** diff --git a/src/Reflection/Constant/RuntimeConstantReflection.php b/src/Reflection/Constant/RuntimeConstantReflection.php index 7bc10879f4..4b31de502d 100644 --- a/src/Reflection/Constant/RuntimeConstantReflection.php +++ b/src/Reflection/Constant/RuntimeConstantReflection.php @@ -2,11 +2,11 @@ namespace PHPStan\Reflection\Constant; -use PHPStan\Reflection\GlobalConstantReflection; +use PHPStan\Reflection\ConstantReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -class RuntimeConstantReflection implements GlobalConstantReflection +final class RuntimeConstantReflection implements ConstantReflection { public function __construct( diff --git a/src/Reflection/ConstantNameHelper.php b/src/Reflection/ConstantNameHelper.php index cd4b526a77..45b65f43a6 100644 --- a/src/Reflection/ConstantNameHelper.php +++ b/src/Reflection/ConstantNameHelper.php @@ -10,7 +10,7 @@ use function str_contains; use function strtolower; -class ConstantNameHelper +final class ConstantNameHelper { public static function normalize(string $name): string diff --git a/src/Reflection/ConstantReflection.php b/src/Reflection/ConstantReflection.php index 5e3d2548c1..ebae755849 100644 --- a/src/Reflection/ConstantReflection.php +++ b/src/Reflection/ConstantReflection.php @@ -2,18 +2,23 @@ namespace PHPStan\Reflection; -use PhpParser\Node\Expr; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Type; /** @api */ -interface ConstantReflection extends ClassMemberReflection, GlobalConstantReflection +interface ConstantReflection { - /** - * @deprecated Use getValueExpr() - * @return mixed - */ - public function getValue(); + public function getName(): string; - public function getValueExpr(): Expr; + 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 index 6a721f2dd7..fc38d72298 100644 --- a/src/Reflection/ConstructorsHelper.php +++ b/src/Reflection/ConstructorsHelper.php @@ -2,11 +2,14 @@ namespace PHPStan\Reflection; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; use ReflectionException; use function array_key_exists; use function explode; +#[AutowiredService] final class ConstructorsHelper { @@ -18,6 +21,7 @@ final class ConstructorsHelper */ public function __construct( private Container $container, + #[AutowiredParameter] private array $additionalConstructors, ) { 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(private ClassReflection $declaringClass, private ExtendedMethodReflection $reflection, private array $variants, private ?array $namedArgumentsVariants) + public function __construct( + private ClassReflection $declaringClass, + private ExtendedMethodReflection $reflection, + private array $variants, + private ?array $namedArgumentsVariants, + private ?Type $selfOutType, + private ?Type $throwType, + ) { } @@ -62,6 +71,16 @@ 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; @@ -92,9 +111,19 @@ 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 @@ -107,9 +136,14 @@ public function getAsserts(): Assertions return $this->reflection->getAsserts(); } + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->reflection->acceptsNamedArguments(); + } + public function getSelfOutType(): ?Type { - return $this->reflection->getSelfOutType(); + return $this->selfOutType; } public function returnsByReference(): TrinaryLogic @@ -132,4 +166,9 @@ public function isPure(): TrinaryLogic return $this->reflection->isPure(); } + public function getAttributes(): array + { + return $this->reflection->getAttributes(); + } + } diff --git a/src/Reflection/Dummy/ChangedTypePropertyReflection.php b/src/Reflection/Dummy/ChangedTypePropertyReflection.php index 018d8593fa..b43cf53453 100644 --- a/src/Reflection/Dummy/ChangedTypePropertyReflection.php +++ b/src/Reflection/Dummy/ChangedTypePropertyReflection.php @@ -3,18 +3,24 @@ namespace PHPStan\Reflection\Dummy; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\WrapperPropertyReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -class ChangedTypePropertyReflection implements WrapperPropertyReflection +final class ChangedTypePropertyReflection implements WrapperPropertyReflection { - public function __construct(private ClassReflection $declaringClass, private PropertyReflection $reflection, private Type $readableType, private Type $writableType) + public function __construct(private ClassReflection $declaringClass, private ExtendedPropertyReflection $reflection, private Type $readableType, private Type $writableType, private Type $phpDocType, private Type $nativeType) { } + public function getName(): string + { + return $this->reflection->getName(); + } + public function getDeclaringClass(): ClassReflection { return $this->declaringClass; @@ -40,6 +46,26 @@ public function getDocComment(): ?string return $this->reflection->getDocComment(); } + public function hasPhpDocType(): bool + { + return $this->reflection->hasPhpDocType(); + } + + public function getPhpDocType(): Type + { + return $this->phpDocType; + } + + public function hasNativeType(): bool + { + return $this->reflection->hasNativeType(); + } + + public function getNativeType(): Type + { + return $this->nativeType; + } + public function getReadableType(): Type { return $this->readableType; @@ -80,9 +106,54 @@ 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(); + } + } diff --git a/src/Reflection/Dummy/DummyConstantReflection.php b/src/Reflection/Dummy/DummyClassConstantReflection.php similarity index 73% rename from src/Reflection/Dummy/DummyConstantReflection.php rename to src/Reflection/Dummy/DummyClassConstantReflection.php index 2f2ef828ec..ffc6afe7c9 100644 --- a/src/Reflection/Dummy/DummyConstantReflection.php +++ b/src/Reflection/Dummy/DummyClassConstantReflection.php @@ -4,15 +4,15 @@ use PhpParser\Node\Expr; use PHPStan\Node\Expr\TypeExpr; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use stdClass; -class DummyConstantReflection implements ConstantReflection +final class DummyClassConstantReflection implements ClassConstantReflection { public function __construct(private string $name) @@ -26,6 +26,11 @@ public function getDeclaringClass(): ClassReflection return $reflectionProvider->getClass(stdClass::class); } + public function isFinal(): bool + { + return false; + } + public function getFileName(): ?string { return null; @@ -51,16 +56,6 @@ public function getName(): string return $this->name; } - /** - * @deprecated - * @return mixed - */ - public function getValue() - { - // so that Scope::getTypeFromValue() returns mixed - return new stdClass(); - } - public function getValueType(): Type { return new MixedType(); @@ -91,4 +86,29 @@ 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/DummyConstructorReflection.php b/src/Reflection/Dummy/DummyConstructorReflection.php index b3cdde365f..c48d6904ce 100644 --- a/src/Reflection/Dummy/DummyConstructorReflection.php +++ b/src/Reflection/Dummy/DummyConstructorReflection.php @@ -5,15 +5,16 @@ use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedFunctionVariant; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; +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 ExtendedMethodReflection +final class DummyConstructorReflection implements ExtendedMethodReflection { public function __construct(private ClassReflection $declaringClass) @@ -53,7 +54,7 @@ public function getPrototype(): ClassMemberReflection public function getVariants(): array { return [ - new FunctionVariantWithPhpDocs( + new ExtendedFunctionVariant( TemplateTypeMap::createEmpty(), null, [], @@ -66,6 +67,11 @@ public function getVariants(): array ]; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + public function getNamedArgumentsVariants(): ?array { return null; @@ -91,6 +97,11 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getThrowType(): ?Type { return null; @@ -111,6 +122,11 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->declaringClass->acceptsNamedArguments()); + } + public function getSelfOutType(): ?Type { return null; @@ -136,4 +152,9 @@ public function isPure(): TrinaryLogic return TrinaryLogic::createYes(); } + public function getAttributes(): array + { + return []; + } + } diff --git a/src/Reflection/Dummy/DummyMethodReflection.php b/src/Reflection/Dummy/DummyMethodReflection.php index 806c24c815..dced9b6206 100644 --- a/src/Reflection/Dummy/DummyMethodReflection.php +++ b/src/Reflection/Dummy/DummyMethodReflection.php @@ -6,13 +6,14 @@ use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; 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 ExtendedMethodReflection +final class DummyMethodReflection implements ExtendedMethodReflection { public function __construct(private string $name) @@ -58,6 +59,11 @@ public function getVariants(): array ]; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + public function getNamedArgumentsVariants(): ?array { return null; @@ -88,6 +94,11 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getThrowType(): ?Type { return null; @@ -108,6 +119,11 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function getSelfOutType(): ?Type { return null; @@ -128,4 +144,9 @@ public function isPure(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getAttributes(): array + { + return []; + } + } diff --git a/src/Reflection/Dummy/DummyPropertyReflection.php b/src/Reflection/Dummy/DummyPropertyReflection.php index b72a011960..c90f546344 100644 --- a/src/Reflection/Dummy/DummyPropertyReflection.php +++ b/src/Reflection/Dummy/DummyPropertyReflection.php @@ -3,16 +3,27 @@ namespace PHPStan\Reflection\Dummy; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use stdClass; -class DummyPropertyReflection implements PropertyReflection +final class DummyPropertyReflection implements ExtendedPropertyReflection { + public function __construct(private string $name) + { + } + + public function getName(): string + { + return $this->name; + } + public function getDeclaringClass(): ClassReflection { $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); @@ -35,6 +46,26 @@ public function isPublic(): bool return true; } + public function hasPhpDocType(): bool + { + return false; + } + + public function getPhpDocType(): Type + { + return new MixedType(); + } + + public function hasNativeType(): bool + { + return false; + } + + public function getNativeType(): Type + { + return new MixedType(); + } + public function getReadableType(): Type { return new MixedType(); @@ -80,4 +111,49 @@ 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 []; + } + } diff --git a/src/Reflection/EnumCaseReflection.php b/src/Reflection/EnumCaseReflection.php index b538f44b3f..94cced2739 100644 --- a/src/Reflection/EnumCaseReflection.php +++ b/src/Reflection/EnumCaseReflection.php @@ -2,14 +2,48 @@ namespace PHPStan\Reflection; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumBackedCase; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumUnitCase; +use PHPStan\Internal\DeprecatedAttributeHelper; +use PHPStan\Reflection\Deprecation\DeprecationProvider; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Type; -/** @api */ -class EnumCaseReflection +/** + * @api + */ +final class EnumCaseReflection { - public function __construct(private ClassReflection $declaringEnum, private string $name, private ?Type $backingValueType) + private bool $isDeprecated; + + private ?string $deprecatedDescription; + + /** + * @param list $attributes + */ + public function __construct( + private ClassReflection $declaringEnum, + private ReflectionEnumUnitCase|ReflectionEnumBackedCase $reflection, + private ?Type $backingValueType, + private array $attributes, + DeprecationProvider $deprecationProvider, + ) { + $deprecation = $deprecationProvider->getEnumCaseDeprecation($reflection); + if ($deprecation !== null) { + $this->isDeprecated = true; + $this->deprecatedDescription = $deprecation->getDescription(); + + } elseif ($reflection->isDeprecated()) { + $attributes = $this->reflection->getBetterReflection()->getAttributes(); + $this->isDeprecated = true; + $this->deprecatedDescription = DeprecatedAttributeHelper::getDeprecatedDescription($attributes); + } else { + $this->isDeprecated = false; + $this->deprecatedDescription = null; + } } public function getDeclaringEnum(): ClassReflection @@ -19,7 +53,15 @@ public function getDeclaringEnum(): ClassReflection public function getName(): string { - return $this->name; + return $this->reflection->getName(); + } + + public function getEnumCaseObjectType(): EnumCaseObjectType + { + return new EnumCaseObjectType( + $this->declaringEnum->getName(), + $this->getName(), + ); } public function getBackingValueType(): ?Type @@ -27,4 +69,22 @@ public function getBackingValueType(): ?Type return $this->backingValueType; } + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->isDeprecated); + } + + public function getDeprecatedDescription(): ?string + { + return $this->deprecatedDescription; + } + + /** + * @return list + */ + public function getAttributes(): array + { + return $this->attributes; + } + } diff --git a/src/Reflection/CallableFunctionVariantWithPhpDocs.php b/src/Reflection/ExtendedCallableFunctionVariant.php similarity index 83% rename from src/Reflection/CallableFunctionVariantWithPhpDocs.php rename to src/Reflection/ExtendedCallableFunctionVariant.php index f9a8c002c6..5e2d3a9c10 100644 --- a/src/Reflection/CallableFunctionVariantWithPhpDocs.php +++ b/src/Reflection/ExtendedCallableFunctionVariant.php @@ -11,11 +11,11 @@ use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; -class CallableFunctionVariantWithPhpDocs extends FunctionVariantWithPhpDocs implements CallableParametersAcceptor +final class ExtendedCallableFunctionVariant extends ExtendedFunctionVariant implements CallableParametersAcceptor { /** - * @param array $parameters + * @param list $parameters * @param SimpleThrowPoint[] $throwPoints * @param SimpleImpurePoint[] $impurePoints * @param InvalidateExprNode[] $invalidateExpressions @@ -35,6 +35,7 @@ public function __construct( private array $impurePoints, private array $invalidateExpressions, private array $usedVariables, + private TrinaryLogic $acceptsNamedArguments, ) { parent::__construct( @@ -74,4 +75,9 @@ public function getUsedVariables(): array return $this->usedVariables; } + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->acceptsNamedArguments; + } + } diff --git a/src/Reflection/FunctionVariantWithPhpDocs.php b/src/Reflection/ExtendedFunctionVariant.php similarity index 76% rename from src/Reflection/FunctionVariantWithPhpDocs.php rename to src/Reflection/ExtendedFunctionVariant.php index 2efd1c1a97..e45f402bb0 100644 --- a/src/Reflection/FunctionVariantWithPhpDocs.php +++ b/src/Reflection/ExtendedFunctionVariant.php @@ -6,13 +6,15 @@ use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; -/** @api */ -class FunctionVariantWithPhpDocs extends FunctionVariant implements ParametersAcceptorWithPhpDocs +/** + * @api + */ +class ExtendedFunctionVariant extends FunctionVariant implements ExtendedParametersAcceptor { /** + * @param list $parameters * @api - * @param array $parameters */ public function __construct( TemplateTypeMap $templateTypeMap, @@ -36,11 +38,11 @@ public function __construct( } /** - * @return array + * @return list */ public function getParameters(): array { - /** @var array $parameters */ + /** @var list $parameters */ $parameters = parent::getParameters(); return $parameters; diff --git a/src/Reflection/ExtendedMethodReflection.php b/src/Reflection/ExtendedMethodReflection.php index 0ff0f8f2de..5cea392754 100644 --- a/src/Reflection/ExtendedMethodReflection.php +++ b/src/Reflection/ExtendedMethodReflection.php @@ -23,15 +23,22 @@ interface ExtendedMethodReflection extends MethodReflection { /** - * @return ParametersAcceptorWithPhpDocs[] + * @return list */ public function getVariants(): array; /** - * @return ParametersAcceptorWithPhpDocs[]|null + * @internal + */ + public function getOnlyVariant(): ExtendedParametersAcceptor; + + /** + * @return list|null */ public function getNamedArgumentsVariants(): ?array; + public function acceptsNamedArguments(): TrinaryLogic; + public function getAsserts(): Assertions; public function getSelfOutType(): ?Type; @@ -42,6 +49,8 @@ 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. @@ -51,4 +60,9 @@ public function isAbstract(): TrinaryLogic|bool; */ public function isPure(): TrinaryLogic; + /** + * @return list + */ + public function getAttributes(): array; + } diff --git a/src/Reflection/ParameterReflectionWithPhpDocs.php b/src/Reflection/ExtendedParameterReflection.php similarity index 64% rename from src/Reflection/ParameterReflectionWithPhpDocs.php rename to src/Reflection/ExtendedParameterReflection.php index 943338a493..ab50b76bd8 100644 --- a/src/Reflection/ParameterReflectionWithPhpDocs.php +++ b/src/Reflection/ExtendedParameterReflection.php @@ -6,11 +6,13 @@ use PHPStan\Type\Type; /** @api */ -interface ParameterReflectionWithPhpDocs extends ParameterReflection +interface ExtendedParameterReflection extends ParameterReflection { public function getPhpDocType(): Type; + public function hasNativeType(): bool; + public function getNativeType(): Type; public function getOutType(): ?Type; @@ -19,4 +21,9 @@ public function isImmediatelyInvokedCallable(): TrinaryLogic; public function getClosureThisType(): ?Type; + /** + * @return list + */ + public function getAttributes(): array; + } diff --git a/src/Reflection/ParametersAcceptorWithPhpDocs.php b/src/Reflection/ExtendedParametersAcceptor.php similarity index 75% rename from src/Reflection/ParametersAcceptorWithPhpDocs.php rename to src/Reflection/ExtendedParametersAcceptor.php index f8ae03e477..77fb213b49 100644 --- a/src/Reflection/ParametersAcceptorWithPhpDocs.php +++ b/src/Reflection/ExtendedParametersAcceptor.php @@ -6,11 +6,11 @@ use PHPStan\Type\Type; /** @api */ -interface ParametersAcceptorWithPhpDocs extends ParametersAcceptor +interface ExtendedParametersAcceptor extends ParametersAcceptor { /** - * @return array + * @return list */ public function getParameters(): array; diff --git a/src/Reflection/ExtendedPropertyReflection.php b/src/Reflection/ExtendedPropertyReflection.php new file mode 100644 index 0000000000..ad8b5a26d8 --- /dev/null +++ b/src/Reflection/ExtendedPropertyReflection.php @@ -0,0 +1,66 @@ + + */ + public function getAttributes(): array; + +} diff --git a/src/Reflection/FunctionReflection.php b/src/Reflection/FunctionReflection.php index 91afcbaadb..297e4dd7d3 100644 --- a/src/Reflection/FunctionReflection.php +++ b/src/Reflection/FunctionReflection.php @@ -14,21 +14,26 @@ public function getName(): string; public function getFileName(): ?string; /** - * @return ParametersAcceptorWithPhpDocs[] + * @return list */ public function getVariants(): array; /** - * @return ParametersAcceptorWithPhpDocs[]|null + * @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; @@ -52,4 +57,9 @@ public function returnsByReference(): TrinaryLogic; */ public function isPure(): TrinaryLogic; + /** + * @return list + */ + public function getAttributes(): array; + } diff --git a/src/Reflection/FunctionReflectionFactory.php b/src/Reflection/FunctionReflectionFactory.php index 67684abdab..993bf34b3b 100644 --- a/src/Reflection/FunctionReflectionFactory.php +++ b/src/Reflection/FunctionReflectionFactory.php @@ -15,6 +15,7 @@ interface FunctionReflectionFactory * @param array $phpDocParameterOutTypes * @param array $phpDocParameterImmediatelyInvokedCallable * @param array $phpDocParameterClosureThisTypes + * @param list $attributes */ public function create( ReflectionFunction $reflection, @@ -25,14 +26,15 @@ public function create( ?string $deprecatedDescription, bool $isDeprecated, bool $isInternal, - bool $isFinal, ?string $filename, ?bool $isPure, Assertions $asserts, + bool $acceptsNamedArguments, ?string $phpDocComment, array $phpDocParameterOutTypes, array $phpDocParameterImmediatelyInvokedCallable, array $phpDocParameterClosureThisTypes, + array $attributes, ): PhpFunctionReflection; } diff --git a/src/Reflection/FunctionVariant.php b/src/Reflection/FunctionVariant.php index 3c5947ac5c..7c69274ef0 100644 --- a/src/Reflection/FunctionVariant.php +++ b/src/Reflection/FunctionVariant.php @@ -6,7 +6,9 @@ use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; -/** @api */ +/** + * @api + */ class FunctionVariant implements ParametersAcceptor { @@ -14,7 +16,7 @@ class FunctionVariant implements ParametersAcceptor /** * @api - * @param array $parameters + * @param list $parameters */ public function __construct( private TemplateTypeMap $templateTypeMap, @@ -44,7 +46,7 @@ public function getCallSiteVarianceMap(): TemplateTypeVarianceMap } /** - * @return array + * @return list */ public function getParameters(): array { diff --git a/src/Reflection/GenericParametersAcceptorResolver.php b/src/Reflection/GenericParametersAcceptorResolver.php index edbb692931..35f70bf37d 100644 --- a/src/Reflection/GenericParametersAcceptorResolver.php +++ b/src/Reflection/GenericParametersAcceptorResolver.php @@ -3,7 +3,7 @@ namespace PHPStan\Reflection; use PHPStan\Reflection\Callables\CallableParametersAcceptor; -use PHPStan\Reflection\Php\DummyParameterWithPhpDocs; +use PHPStan\Reflection\Php\ExtendedDummyParameter; use PHPStan\TrinaryLogic; use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\ErrorType; @@ -18,14 +18,14 @@ use function count; use function is_int; -class GenericParametersAcceptorResolver +final class GenericParametersAcceptorResolver { /** * @api * @param array $argTypes */ - public static function resolve(array $argTypes, ParametersAcceptor $parametersAcceptor): ParametersAcceptorWithPhpDocs + public static function resolve(array $argTypes, ParametersAcceptor $parametersAcceptor): ExtendedParametersAcceptor { $typeMap = TemplateTypeMap::createEmpty(); $passedArgs = []; @@ -87,11 +87,11 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc $originalParametersAcceptor = $parametersAcceptor; - if (!$parametersAcceptor instanceof ParametersAcceptorWithPhpDocs) { - $parametersAcceptor = new FunctionVariantWithPhpDocs( + if (!$parametersAcceptor instanceof ExtendedParametersAcceptor) { + $parametersAcceptor = new ExtendedFunctionVariant( $parametersAcceptor->getTemplateTypeMap(), $parametersAcceptor->getResolvedTemplateTypeMap(), - array_map(static fn (ParameterReflection $parameter): ParameterReflectionWithPhpDocs => new DummyParameterWithPhpDocs( + array_map(static fn (ParameterReflection $parameter): ExtendedParameterReflection => new ExtendedDummyParameter( $parameter->getName(), $parameter->getType(), $parameter->isOptional(), @@ -103,6 +103,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc null, TrinaryLogic::createMaybe(), null, + [], ), $parametersAcceptor->getParameters()), $parametersAcceptor->isVariadic(), $parametersAcceptor->getReturnType(), @@ -126,6 +127,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc $originalParametersAcceptor->getImpurePoints(), $originalParametersAcceptor->getInvalidateExpressions(), $originalParametersAcceptor->getUsedVariables(), + $originalParametersAcceptor->acceptsNamedArguments(), ); } 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; } @@ -37,9 +37,6 @@ public function getCallSiteVarianceMap(): TemplateTypeVarianceMap return TemplateTypeVarianceMap::createEmpty(); } - /** - * @return array - */ public function getParameters(): array { return []; @@ -86,4 +83,9 @@ public function getUsedVariables(): array return []; } + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->methodReflection->acceptsNamedArguments(); + } + } diff --git a/src/Reflection/InitializerExprContext.php b/src/Reflection/InitializerExprContext.php index c6e2f890a8..e6fb8ab6e5 100644 --- a/src/Reflection/InitializerExprContext.php +++ b/src/Reflection/InitializerExprContext.php @@ -2,12 +2,14 @@ namespace PHPStan\Reflection; +use PhpParser\Node\PropertyHook; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; use PHPStan\Analyser\Scope; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; use PHPStan\BetterReflection\Reflection\ReflectionConstant; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\ShouldNotHappenException; use function array_slice; use function count; @@ -15,8 +17,10 @@ use function implode; use function sprintf; -/** @api */ -class InitializerExprContext implements NamespaceAnswerer +/** + * @api + */ +final class InitializerExprContext implements NamespaceAnswerer { /** @@ -29,21 +33,25 @@ private function __construct( private ?string $traitName, private ?string $function, private ?string $method, + private ?string $property, ) { } public static function fromScope(Scope $scope): self { + $function = $scope->getFunction(); + return new self( $scope->getFile(), $scope->getNamespace(), $scope->isInClass() ? $scope->getClassReflection()->getName() : null, $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, - $scope->isInAnonymousFunction() ? '{closure}' : ($scope->getFunction() !== null ? $scope->getFunction()->getName() : null), - $scope->isInAnonymousFunction() ? '{closure}' : ($scope->getFunction() instanceof MethodReflection - ? sprintf('%s::%s', $scope->getFunction()->getDeclaringClass()->getName(), $scope->getFunction()->getName()) - : ($scope->getFunction() instanceof FunctionReflection ? $scope->getFunction()->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, ); } @@ -78,6 +86,33 @@ public static function fromClass(string $className, ?string $fileName): self 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, ); } @@ -93,6 +128,7 @@ public static function fromReflectionParameter(ReflectionParameter $parameter): null, $declaringFunction->getName(), $declaringFunction->getName(), + null, // Property hook parameter cannot have a default value. fromReflectionParameter is only used for that ); } @@ -107,13 +143,14 @@ public static function fromReflectionParameter(ReflectionParameter $parameter): $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_ $function, + ClassMethod|Function_|PropertyHook $function, ): self { $namespace = null; @@ -124,15 +161,36 @@ public static function fromStubParameter( $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, - $function instanceof Function_ && $function->namespacedName !== null ? $function->namespacedName->toString() : ($function instanceof ClassMethod ? $function->name->toString() : null), - $function instanceof ClassMethod && $className !== null - ? sprintf('%s::%s', $className, $function->name->toString()) - : ($function instanceof Function_ && $function->namespacedName !== null ? $function->namespacedName->toString() : null), + $functionName, + $methodName, + $propertyName, ); } @@ -145,12 +203,13 @@ public static function fromGlobalConstant(ReflectionConstant $constant): self null, null, null, + null, ); } public static function createEmpty(): self { - return new self(null, null, null, null, null, null); + return new self(null, null, null, null, null, null, null); } public function getFile(): ?string @@ -183,4 +242,9 @@ 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 index 3615ac70b4..af6302edfc 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -11,8 +11,8 @@ use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Identifier; use PhpParser\Node\Name; -use PhpParser\Node\Scalar\DNumber; -use PhpParser\Node\Scalar\LNumber; +use PhpParser\Node\Scalar\Float_; +use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Scalar\MagicConst; use PhpParser\Node\Scalar\MagicConst\Dir; use PhpParser\Node\Scalar\MagicConst\File; @@ -20,6 +20,8 @@ use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\OutOfClassScope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Php\PhpVersion; @@ -27,9 +29,11 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; @@ -64,6 +68,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; +use PHPStan\Type\TypeResult; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; @@ -88,7 +93,8 @@ use function strtolower; use const INF; -class InitializerExprTypeResolver +#[AutowiredService] +final class InitializerExprTypeResolver { public const CALCULATE_SCALARS_LIMIT = 128; @@ -102,7 +108,8 @@ public function __construct( private PhpVersion $phpVersion, private OperatorTypeSpecifyingExtensionRegistryProvider $operatorTypeSpecifyingExtensionRegistryProvider, private OversizedArrayBuilder $oversizedArrayBuilder, - private bool $usePathConstantsAsConstantString = false, + #[AutowiredParameter] + private bool $usePathConstantsAsConstantString, ) { } @@ -113,10 +120,10 @@ public function getType(Expr $expr, InitializerExprContext $context): Type if ($expr instanceof TypeExpr) { return $expr->getExprType(); } - if ($expr instanceof LNumber) { + if ($expr instanceof Int_) { return new ConstantIntegerType($expr->value); } - if ($expr instanceof DNumber) { + if ($expr instanceof Float_) { return new ConstantFloatType($expr->value); } if ($expr instanceof String_) { @@ -297,7 +304,7 @@ public function getType(Expr $expr, InitializerExprContext $context): Type return $this->resolveIdenticalType( $this->getType($expr->left, $context), $this->getType($expr->right, $context), - ); + )->type; } if ($expr instanceof BinaryOp\NotIdentical) { @@ -308,7 +315,7 @@ public function getType(Expr $expr, InitializerExprContext $context): Type return $this->resolveEqualType( $this->getType($expr->left, $context), $this->getType($expr->right, $context), - ); + )->type; } if ($expr instanceof BinaryOp\NotEqual) { @@ -316,19 +323,19 @@ public function getType(Expr $expr, InitializerExprContext $context): Type } if ($expr instanceof Expr\BinaryOp\Smaller) { - return $this->getType($expr->left, $context)->isSmallerThan($this->getType($expr->right, $context))->toBooleanType(); + 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))->toBooleanType(); + 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))->toBooleanType(); + 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))->toBooleanType(); + return $this->getType($expr->right, $context)->isSmallerThanOrEqual($this->getType($expr->left, $context), $this->phpVersion)->toBooleanType(); } if ($expr instanceof Expr\BinaryOp\LogicalXor) { @@ -389,6 +396,15 @@ public function getType(Expr $expr, InitializerExprContext $context): Type return new ConstantStringType($context->getTraitName(), true); } + if ($expr instanceof MagicConst\Property) { + $contextProperty = $context->getProperty(); + if ($contextProperty === null) { + return new ConstantStringType(''); + } + + return new ConstantStringType($contextProperty); + } + if ($expr instanceof PropertyFetch && $expr->name instanceof Identifier) { $fetchedOnType = $this->getType($expr->var, $context); if (!$fetchedOnType->hasProperty($expr->name->name)->yes()) { @@ -479,6 +495,14 @@ public function resolveConcatType(Type $left, Type $right): Type $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; @@ -525,10 +549,6 @@ public function getArrayType(Expr\Array_ $expr, callable $getTypeCallback): Type $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); $isList = null; foreach ($expr->items as $arrayItem) { - if ($arrayItem === null) { - continue; - } - $valueType = $getTypeCallback($arrayItem->value); if ($arrayItem->unpack) { $constantArrays = $valueType->getConstantArrays(); @@ -572,7 +592,7 @@ public function getArrayType(Expr\Array_ $expr, callable $getTypeCallback): Type $arrayType = $arrayBuilder->getArray(); if ($isList === true) { - return AccessoryArrayListType::intersectWith($arrayType); + return TypeCombinator::intersect($arrayType, new AccessoryArrayListType()); } return $arrayType; @@ -597,31 +617,33 @@ public function getBitwiseAndType(Expr $left, Expr $right, callable $getTypeCall if ($leftTypesCount > 0 && $rightTypesCount > 0) { $resultTypes = []; $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; - foreach ($leftTypes as $leftTypeInner) { - foreach ($rightTypes as $rightTypeInner) { - if ($leftTypeInner instanceof ConstantStringType && $rightTypeInner instanceof ConstantStringType) { - $resultType = $this->getTypeFromValue($leftTypeInner->getValue() & $rightTypeInner->getValue()); - } else { - $leftNumberType = $leftTypeInner->toNumber(); - $rightNumberType = $rightTypeInner->toNumber(); + if (!$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 ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } - if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { - throw new ShouldNotHappenException(); - } + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } - $resultType = $this->getTypeFromValue($leftNumberType->getValue() & $rightNumberType->getValue()); - } - if ($generalize) { - $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + $resultType = $this->getTypeFromValue($leftNumberType->getValue() & $rightNumberType->getValue()); + } + $resultTypes[] = $resultType; } - $resultTypes[] = $resultType; } + return TypeCombinator::union(...$resultTypes); } - return TypeCombinator::union(...$resultTypes); + + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); } if ($leftType->isString()->yes() && $rightType->isString()->yes()) { @@ -664,31 +686,33 @@ public function getBitwiseOrType(Expr $left, Expr $right, callable $getTypeCallb if ($leftTypesCount > 0 && $rightTypesCount > 0) { $resultTypes = []; $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; - foreach ($leftTypes as $leftTypeInner) { - foreach ($rightTypes as $rightTypeInner) { - if ($leftTypeInner instanceof ConstantStringType && $rightTypeInner instanceof ConstantStringType) { - $resultType = $this->getTypeFromValue($leftTypeInner->getValue() | $rightTypeInner->getValue()); - } else { - $leftNumberType = $leftTypeInner->toNumber(); - $rightNumberType = $rightTypeInner->toNumber(); + if (!$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 ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } - if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { - throw new ShouldNotHappenException(); - } + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } - $resultType = $this->getTypeFromValue($leftNumberType->getValue() | $rightNumberType->getValue()); - } - if ($generalize) { - $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + $resultType = $this->getTypeFromValue($leftNumberType->getValue() | $rightNumberType->getValue()); + } + $resultTypes[] = $resultType; } - $resultTypes[] = $resultType; } + return TypeCombinator::union(...$resultTypes); } - return TypeCombinator::union(...$resultTypes); + + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); } if ($leftType->isString()->yes() && $rightType->isString()->yes()) { @@ -721,31 +745,33 @@ public function getBitwiseXorType(Expr $left, Expr $right, callable $getTypeCall if ($leftTypesCount > 0 && $rightTypesCount > 0) { $resultTypes = []; $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; - foreach ($leftTypes as $leftTypeInner) { - foreach ($rightTypes as $rightTypeInner) { - if ($leftTypeInner instanceof ConstantStringType && $rightTypeInner instanceof ConstantStringType) { - $resultType = $this->getTypeFromValue($leftTypeInner->getValue() ^ $rightTypeInner->getValue()); - } else { - $leftNumberType = $leftTypeInner->toNumber(); - $rightNumberType = $rightTypeInner->toNumber(); + if (!$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 ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } - if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { - throw new ShouldNotHappenException(); - } + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } - $resultType = $this->getTypeFromValue($leftNumberType->getValue() ^ $rightNumberType->getValue()); - } - if ($generalize) { - $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + $resultType = $this->getTypeFromValue($leftNumberType->getValue() ^ $rightNumberType->getValue()); + } + $resultTypes[] = $resultType; } - $resultTypes[] = $resultType; } + return TypeCombinator::union(...$resultTypes); } - return TypeCombinator::union(...$resultTypes); + + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); } if ($leftType->isString()->yes() && $rightType->isString()->yes()) { @@ -807,36 +833,38 @@ public function getDivType(Expr $left, Expr $right, callable $getTypeCallback): if ($leftTypesCount > 0 && $rightTypesCount > 0) { $resultTypes = []; $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; - foreach ($leftTypes as $leftTypeInner) { - foreach ($rightTypes as $rightTypeInner) { - $leftNumberType = $leftTypeInner->toNumber(); - $rightNumberType = $rightTypeInner->toNumber(); + if (!$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 ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } - if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { - throw new ShouldNotHappenException(); - } + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } - if (in_array($rightNumberType->getValue(), [0, 0.0], true)) { - return new ErrorType(); - } + if (in_array($rightNumberType->getValue(), [0, 0.0], true)) { + return new ErrorType(); + } - $resultType = $this->getTypeFromValue($leftNumberType->getValue() / $rightNumberType->getValue()); // @phpstan-ignore binaryOp.invalid - if ($generalize) { - $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + $resultType = $this->getTypeFromValue($leftNumberType->getValue() / $rightNumberType->getValue()); // @phpstan-ignore binaryOp.invalid + $resultTypes[] = $resultType; } - $resultTypes[] = $resultType; } + return TypeCombinator::union(...$resultTypes); } - return TypeCombinator::union(...$resultTypes); + + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); } $rightScalarValues = $rightType->toNumber()->getConstantScalarValues(); foreach ($rightScalarValues as $scalarValue) { - if ($scalarValue === 0 || $scalarValue === 0.0) { + if (in_array($scalarValue, [0, 0.0], true)) { return new ErrorType(); } } @@ -856,6 +884,11 @@ public function getModType(Expr $left, Expr $right, callable $getTypeCallback): return $this->getNeverType($leftType, $rightType); } + $extensionSpecified = $this->callOperatorTypeSpecifyingExtensions(new BinaryOp\Mod($left, $right), $leftType, $rightType); + if ($extensionSpecified !== null) { + return $extensionSpecified; + } + if ($leftType->toNumber() instanceof ErrorType || $rightType->toNumber() instanceof ErrorType) { return new ErrorType(); } @@ -867,32 +900,34 @@ public function getModType(Expr $left, Expr $right, callable $getTypeCallback): if ($leftTypesCount > 0 && $rightTypesCount > 0) { $resultTypes = []; $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; - foreach ($leftTypes as $leftTypeInner) { - foreach ($rightTypes as $rightTypeInner) { - $leftNumberType = $leftTypeInner->toNumber(); - $rightNumberType = $rightTypeInner->toNumber(); + if (!$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 ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } - if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { - throw new ShouldNotHappenException(); - } + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } - $rightIntegerValue = (int) $rightNumberType->getValue(); - if ($rightIntegerValue === 0) { - return new ErrorType(); - } + $rightIntegerValue = (int) $rightNumberType->getValue(); + if ($rightIntegerValue === 0) { + return new ErrorType(); + } - $resultType = $this->getTypeFromValue((int) $leftNumberType->getValue() % $rightIntegerValue); - if ($generalize) { - $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + $resultType = $this->getTypeFromValue((int) $leftNumberType->getValue() % $rightIntegerValue); + $resultTypes[] = $resultType; } - $resultTypes[] = $resultType; } + return TypeCombinator::union(...$resultTypes); } - return TypeCombinator::union(...$resultTypes); + + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); } $integerType = $rightType->toInteger(); @@ -903,7 +938,7 @@ public function getModType(Expr $left, Expr $right, callable $getTypeCallback): $rightScalarValues = $rightType->toNumber()->getConstantScalarValues(); foreach ($rightScalarValues as $scalarValue) { - if ($scalarValue === 0 || $scalarValue === 0.0) { + if (in_array($scalarValue, [0, 0.0], true)) { return new ErrorType(); } } @@ -964,28 +999,30 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): if ($leftTypesCount > 0 && $rightTypesCount > 0) { $resultTypes = []; $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; - foreach ($leftTypes as $leftTypeInner) { - foreach ($rightTypes as $rightTypeInner) { - $leftNumberType = $leftTypeInner->toNumber(); - $rightNumberType = $rightTypeInner->toNumber(); + if (!$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 ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } - if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { - throw new ShouldNotHappenException(); - } + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } - $resultType = $this->getTypeFromValue($leftNumberType->getValue() + $rightNumberType->getValue()); - if ($generalize) { - $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + $resultType = $this->getTypeFromValue($leftNumberType->getValue() + $rightNumberType->getValue()); + $resultTypes[] = $resultType; } - $resultTypes[] = $resultType; } + + return TypeCombinator::union(...$resultTypes); } - return TypeCombinator::union(...$resultTypes); + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); } $leftConstantArrays = $leftType->getConstantArrays(); @@ -1045,7 +1082,7 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); } if ($leftType->isList()->yes() && $rightType->isList()->yes()) { - $arrayType = AccessoryArrayListType::intersectWith($arrayType); + $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType()); } return $arrayType; @@ -1126,28 +1163,30 @@ public function getMinusType(Expr $left, Expr $right, callable $getTypeCallback) if ($leftTypesCount > 0 && $rightTypesCount > 0) { $resultTypes = []; $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; - foreach ($leftTypes as $leftTypeInner) { - foreach ($rightTypes as $rightTypeInner) { - $leftNumberType = $leftTypeInner->toNumber(); - $rightNumberType = $rightTypeInner->toNumber(); + if (!$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 ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } - if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { - throw new ShouldNotHappenException(); - } + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } - $resultType = $this->getTypeFromValue($leftNumberType->getValue() - $rightNumberType->getValue()); - if ($generalize) { - $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + $resultType = $this->getTypeFromValue($leftNumberType->getValue() - $rightNumberType->getValue()); + $resultTypes[] = $resultType; } - $resultTypes[] = $resultType; } + + return TypeCombinator::union(...$resultTypes); } - return TypeCombinator::union(...$resultTypes); + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); } return $this->resolveCommonMath(new BinaryOp\Minus($left, $right), $leftType, $rightType); @@ -1168,41 +1207,42 @@ public function getMulType(Expr $left, Expr $right, callable $getTypeCallback): if ($leftTypesCount > 0 && $rightTypesCount > 0) { $resultTypes = []; $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; - foreach ($leftTypes as $leftTypeInner) { - foreach ($rightTypes as $rightTypeInner) { - $leftNumberType = $leftTypeInner->toNumber(); - $rightNumberType = $rightTypeInner->toNumber(); + if (!$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 ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } - if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { - throw new ShouldNotHappenException(); - } + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } - $resultType = $this->getTypeFromValue($leftNumberType->getValue() * $rightNumberType->getValue()); - if ($generalize) { - $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + $resultType = $this->getTypeFromValue($leftNumberType->getValue() * $rightNumberType->getValue()); + $resultTypes[] = $resultType; } - $resultTypes[] = $resultType; } + + return TypeCombinator::union(...$resultTypes); } - return TypeCombinator::union(...$resultTypes); + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); } - $floatType = new FloatType(); $leftNumberType = $leftType->toNumber(); if ($leftNumberType instanceof ConstantIntegerType && $leftNumberType->getValue() === 0) { - if ($floatType->isSuperTypeOf($rightType)->yes()) { + if ($rightType->isFloat()->yes()) { return new ConstantFloatType(0.0); } return new ConstantIntegerType(0); } $rightNumberType = $rightType->toNumber(); if ($rightNumberType instanceof ConstantIntegerType && $rightNumberType->getValue() === 0) { - if ($floatType->isSuperTypeOf($leftType)->yes()) { + if ($leftType->isFloat()->yes()) { return new ConstantFloatType(0.0); } return new ConstantIntegerType(0); @@ -1219,16 +1259,16 @@ public function getPowType(Expr $left, Expr $right, callable $getTypeCallback): $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); - $exponentiatedTyped = $leftType->exponentiate($rightType); - if (!$exponentiatedTyped instanceof ErrorType) { - return $exponentiatedTyped; - } - $extensionSpecified = $this->callOperatorTypeSpecifyingExtensions(new BinaryOp\Pow($left, $right), $leftType, $rightType); if ($extensionSpecified !== null) { return $extensionSpecified; } + $exponentiatedTyped = $leftType->exponentiate($rightType); + if (!$exponentiatedTyped instanceof ErrorType) { + return $exponentiatedTyped; + } + return new ErrorType(); } @@ -1251,32 +1291,34 @@ public function getShiftLeftType(Expr $left, Expr $right, callable $getTypeCallb if ($leftTypesCount > 0 && $rightTypesCount > 0) { $resultTypes = []; $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; - foreach ($leftTypes as $leftTypeInner) { - foreach ($rightTypes as $rightTypeInner) { - $leftNumberType = $leftTypeInner->toNumber(); - $rightNumberType = $rightTypeInner->toNumber(); + if (!$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 ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } - if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { - throw new ShouldNotHappenException(); - } + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } - if ($rightNumberType->getValue() < 0) { - return new ErrorType(); - } + if ($rightNumberType->getValue() < 0) { + return new ErrorType(); + } - $resultType = $this->getTypeFromValue(intval($leftNumberType->getValue()) << intval($rightNumberType->getValue())); - if ($generalize) { - $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + $resultType = $this->getTypeFromValue(intval($leftNumberType->getValue()) << intval($rightNumberType->getValue())); + $resultTypes[] = $resultType; } - $resultTypes[] = $resultType; } + + return TypeCombinator::union(...$resultTypes); } - return TypeCombinator::union(...$resultTypes); + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); } $leftNumberType = $leftType->toNumber(); @@ -1308,32 +1350,34 @@ public function getShiftRightType(Expr $left, Expr $right, callable $getTypeCall if ($leftTypesCount > 0 && $rightTypesCount > 0) { $resultTypes = []; $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; - foreach ($leftTypes as $leftTypeInner) { - foreach ($rightTypes as $rightTypeInner) { - $leftNumberType = $leftTypeInner->toNumber(); - $rightNumberType = $rightTypeInner->toNumber(); + if (!$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 ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } - if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { - throw new ShouldNotHappenException(); - } + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } - if ($rightNumberType->getValue() < 0) { - return new ErrorType(); - } + if ($rightNumberType->getValue() < 0) { + return new ErrorType(); + } - $resultType = $this->getTypeFromValue(intval($leftNumberType->getValue()) >> intval($rightNumberType->getValue())); - if ($generalize) { - $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + $resultType = $this->getTypeFromValue(intval($leftNumberType->getValue()) >> intval($rightNumberType->getValue())); + $resultTypes[] = $resultType; } - $resultTypes[] = $resultType; } + + return TypeCombinator::union(...$resultTypes); } - return TypeCombinator::union(...$resultTypes); + $leftType = $this->optimizeScalarType($leftType); + $rightType = $this->optimizeScalarType($rightType); } $leftNumberType = $leftType->toNumber(); @@ -1346,34 +1390,69 @@ public function getShiftRightType(Expr $left, Expr $right, callable $getTypeCall return $this->resolveCommonMath(new Expr\BinaryOp\ShiftRight($left, $right), $leftType, $rightType); } - public function resolveIdenticalType(Type $leftType, Type $rightType): BooleanType + 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 ConstantBooleanType(false); + return new TypeResult(new ConstantBooleanType(false), []); } if ($leftType instanceof ConstantScalarType && $rightType instanceof ConstantScalarType) { - return new ConstantBooleanType($leftType->getValue() === $rightType->getValue()); + return new TypeResult(new ConstantBooleanType($leftType->getValue() === $rightType->getValue()), []); } $leftTypeFiniteTypes = $leftType->getFiniteTypes(); $rightTypeFiniteType = $rightType->getFiniteTypes(); if (count($leftTypeFiniteTypes) === 1 && count($rightTypeFiniteType) === 1) { - return new ConstantBooleanType($leftTypeFiniteTypes[0]->equals($rightTypeFiniteType[0])); + return new TypeResult(new ConstantBooleanType($leftTypeFiniteTypes[0]->equals($rightTypeFiniteType[0])), []); } - if ($leftType->isSuperTypeOf($rightType)->no() && $rightType->isSuperTypeOf($leftType)->no()) { - return new ConstantBooleanType(false); + $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): BooleanType => $this->resolveIdenticalType($leftValueType, $rightValueType)); + return $this->resolveConstantArrayTypeComparison($leftType, $rightType, fn ($leftValueType, $rightValueType): TypeResult => $this->resolveIdenticalType($leftValueType, $rightValueType)); } - return new BooleanType(); + return new TypeResult(new BooleanType(), []); } - public function resolveEqualType(Type $leftType, Type $rightType): BooleanType + /** + * @return TypeResult + */ + public function resolveEqualType(Type $leftType, Type $rightType): TypeResult { if ( ($leftType->isEnum()->yes() && $rightType->isTrue()->no()) @@ -1383,16 +1462,17 @@ public function resolveEqualType(Type $leftType, Type $rightType): BooleanType } if ($leftType instanceof ConstantArrayType && $rightType instanceof ConstantArrayType) { - return $this->resolveConstantArrayTypeComparison($leftType, $rightType, fn ($leftValueType, $rightValueType): BooleanType => $this->resolveEqualType($leftValueType, $rightValueType)); + return $this->resolveConstantArrayTypeComparison($leftType, $rightType, fn ($leftValueType, $rightValueType): TypeResult => $this->resolveEqualType($leftValueType, $rightValueType)); } - return $leftType->looseCompare($rightType, $this->phpVersion); + return new TypeResult($leftType->looseCompare($rightType, $this->phpVersion), []); } /** - * @param callable(Type, Type): BooleanType $valueComparisonCallback + * @param callable(Type, Type): TypeResult $valueComparisonCallback + * @return TypeResult */ - private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, ConstantArrayType $rightType, callable $valueComparisonCallback): BooleanType + private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, ConstantArrayType $rightType, callable $valueComparisonCallback): TypeResult { $leftKeyTypes = $leftType->getKeyTypes(); $rightKeyTypes = $rightType->getKeyTypes(); @@ -1409,7 +1489,7 @@ private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, if (count($rightKeyTypes) === 0) { if (!$leftOptional) { - return new ConstantBooleanType(false); + return new TypeResult(new ConstantBooleanType(false), []); } continue; } @@ -1422,13 +1502,13 @@ private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, $found = true; break; } elseif (!$rightType->isOptionalKey($j)) { - return new ConstantBooleanType(false); + return new TypeResult(new ConstantBooleanType(false), []); } } if (!$found) { if (!$leftOptional) { - return new ConstantBooleanType(false); + return new TypeResult(new ConstantBooleanType(false), []); } continue; } @@ -1445,21 +1525,22 @@ private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, } } - $leftIdenticalToRight = $valueComparisonCallback($leftValueTypes[$i], $rightValueTypes[$j]); + $leftIdenticalToRightResult = $valueComparisonCallback($leftValueTypes[$i], $rightValueTypes[$j]); + $leftIdenticalToRight = $leftIdenticalToRightResult->type; if ($leftIdenticalToRight->isFalse()->yes()) { - return new ConstantBooleanType(false); + return $leftIdenticalToRightResult; } $resultType = TypeCombinator::union($resultType, $leftIdenticalToRight); } foreach (array_keys($rightKeyTypes) as $j) { if (!$rightType->isOptionalKey($j)) { - return new ConstantBooleanType(false); + return new TypeResult(new ConstantBooleanType(false), []); } $resultType = new BooleanType(); } - return $resultType->toBoolean(); + return new TypeResult($resultType->toBoolean(), []); } private function callOperatorTypeSpecifyingExtensions(Expr\BinaryOp $expr, Type $leftType, Type $rightType): ?Type @@ -1917,7 +1998,7 @@ function (Type $type, callable $traverse): Type { ); } - if ($constantClassType->isClassStringType()->yes()) { + if ($constantClassType->isClassString()->yes()) { if ($constantClassType->isConstantScalarValue()->yes()) { $isObject = false; } @@ -1958,7 +2039,7 @@ function (Type $type, callable $traverse): Type { $constantType = $this->getType($reflectionConstant->getValueExpression(), InitializerExprContext::fromClass($reflectionConstantDeclaringClass->getName(), $reflectionConstantDeclaringClass->getFileName() ?: null)); $nativeType = null; if ($reflectionConstant->getType() !== null) { - $nativeType = TypehintHelper::decideTypeFromReflection($reflectionConstant->getType(), null, $constantClassReflection); + $nativeType = TypehintHelper::decideTypeFromReflection($reflectionConstant->getType(), selfClass: $constantClassReflection); } $types[] = $this->constantResolver->resolveClassConstantType( $constantClassReflection->getName(), @@ -1973,6 +2054,7 @@ function (Type $type, callable $traverse): Type { $constantReflection = $constantClassReflection->getConstant($constantName); if ( !$constantClassReflection->isFinal() + && !$constantReflection->isFinal() && !$constantReflection->hasPhpDocType() && !$constantReflection->hasNativeType() ) { @@ -2048,7 +2130,7 @@ public function getUnaryMinusType(Expr $expr, callable $getTypeCallback): Type } if ($type instanceof IntegerRangeType) { - return $getTypeCallback(new Expr\BinaryOp\Mul($expr, new LNumber(-1))); + return $getTypeCallback(new Expr\BinaryOp\Mul($expr, new Int_(-1))); } return $type; diff --git a/src/Reflection/MethodPrototypeReflection.php b/src/Reflection/MethodPrototypeReflection.php index cf1a6ff76b..b47b4930cb 100644 --- a/src/Reflection/MethodPrototypeReflection.php +++ b/src/Reflection/MethodPrototypeReflection.php @@ -4,7 +4,7 @@ use PHPStan\Type\Type; -class MethodPrototypeReflection implements ClassMemberReflection +final class MethodPrototypeReflection implements ClassMemberReflection { /** @@ -17,7 +17,6 @@ public function __construct( private bool $isPrivate, private bool $isPublic, private bool $isAbstract, - private bool $isFinal, private bool $isInternal, private array $variants, private ?Type $tentativeReturnType, @@ -55,11 +54,6 @@ public function isAbstract(): bool return $this->isAbstract; } - public function isFinal(): bool - { - return $this->isFinal; - } - public function isInternal(): bool { return $this->isInternal; diff --git a/src/Reflection/MethodReflection.php b/src/Reflection/MethodReflection.php index 8d601e9471..529a5011dd 100644 --- a/src/Reflection/MethodReflection.php +++ b/src/Reflection/MethodReflection.php @@ -14,7 +14,7 @@ public function getName(): string; public function getPrototype(): ClassMemberReflection; /** - * @return ParametersAcceptor[] + * @return list */ public function getVariants(): array; diff --git a/src/Reflection/MissingConstantFromReflectionException.php b/src/Reflection/MissingConstantFromReflectionException.php index 230ba5902d..e57d3c3f4f 100644 --- a/src/Reflection/MissingConstantFromReflectionException.php +++ b/src/Reflection/MissingConstantFromReflectionException.php @@ -5,7 +5,7 @@ use Exception; use function sprintf; -class MissingConstantFromReflectionException extends Exception +final class MissingConstantFromReflectionException extends Exception { public function __construct( diff --git a/src/Reflection/MissingMethodFromReflectionException.php b/src/Reflection/MissingMethodFromReflectionException.php index 49c7778cd6..48051aafc1 100644 --- a/src/Reflection/MissingMethodFromReflectionException.php +++ b/src/Reflection/MissingMethodFromReflectionException.php @@ -5,7 +5,7 @@ use Exception; use function sprintf; -class MissingMethodFromReflectionException extends Exception +final class MissingMethodFromReflectionException extends Exception { public function __construct( diff --git a/src/Reflection/MissingPropertyFromReflectionException.php b/src/Reflection/MissingPropertyFromReflectionException.php index 4d62565c1f..2e64aee94c 100644 --- a/src/Reflection/MissingPropertyFromReflectionException.php +++ b/src/Reflection/MissingPropertyFromReflectionException.php @@ -5,7 +5,7 @@ use Exception; use function sprintf; -class MissingPropertyFromReflectionException extends Exception +final class MissingPropertyFromReflectionException extends Exception { public function __construct( diff --git a/src/Reflection/Mixin/MixinMethodReflection.php b/src/Reflection/Mixin/MixinMethodReflection.php index 745a8cae15..15fc4d8ad8 100644 --- a/src/Reflection/Mixin/MixinMethodReflection.php +++ b/src/Reflection/Mixin/MixinMethodReflection.php @@ -8,7 +8,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -class MixinMethodReflection implements MethodReflection +final class MixinMethodReflection implements MethodReflection { public function __construct(private MethodReflection $reflection, private bool $static) diff --git a/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php b/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php index 484690a8b7..58ae58ca2f 100644 --- a/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php +++ b/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php @@ -11,7 +11,7 @@ use function array_intersect; use function count; -class MixinMethodsClassReflectionExtension implements MethodsClassReflectionExtension +final class MixinMethodsClassReflectionExtension implements MethodsClassReflectionExtension { /** @var array> */ @@ -74,6 +74,15 @@ private function findMethod(ClassReflection $classReflection, string $methodName return new MixinMethodReflection($method, $static); } + foreach ($classReflection->getTraits() as $traitClass) { + $methodWithDeclaringClass = $this->findMethod($traitClass, $methodName); + if ($methodWithDeclaringClass === null) { + continue; + } + + return $methodWithDeclaringClass; + } + $parentClass = $classReflection->getParentClass(); while ($parentClass !== null) { $method = $this->findMethod($parentClass, $methodName); diff --git a/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php b/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php index b891da3894..4b21f92451 100644 --- a/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php +++ b/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php @@ -11,7 +11,7 @@ use function array_intersect; use function count; -class MixinPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension +final class MixinPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension { /** @var array> */ @@ -65,6 +65,15 @@ private function findProperty(ClassReflection $classReflection, string $property return $property; } + foreach ($classReflection->getTraits() as $traitClass) { + $methodWithDeclaringClass = $this->findProperty($traitClass, $propertyName); + if ($methodWithDeclaringClass === null) { + continue; + } + + return $methodWithDeclaringClass; + } + $parentClass = $classReflection->getParentClass(); while ($parentClass !== null) { $property = $this->findProperty($parentClass, $propertyName); diff --git a/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php b/src/Reflection/Native/ExtendedNativeParameterReflection.php similarity index 70% rename from src/Reflection/Native/NativeParameterWithPhpDocsReflection.php rename to src/Reflection/Native/ExtendedNativeParameterReflection.php index 3a81fbb4da..00e2ea1a99 100644 --- a/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php +++ b/src/Reflection/Native/ExtendedNativeParameterReflection.php @@ -2,14 +2,19 @@ namespace PHPStan\Reflection\Native; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; +use PHPStan\Reflection\AttributeReflection; +use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; -class NativeParameterWithPhpDocsReflection implements ParameterReflectionWithPhpDocs +final class ExtendedNativeParameterReflection implements ExtendedParameterReflection { + /** + * @param list $attributes + */ public function __construct( private string $name, private bool $optional, @@ -22,6 +27,7 @@ public function __construct( private ?Type $outType, private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, + private array $attributes, ) { } @@ -46,6 +52,11 @@ 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; @@ -81,24 +92,9 @@ public function getClosureThisType(): ?Type return $this->closureThisType; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self + public function getAttributes(): array { - return new self( - $properties['name'], - $properties['optional'], - $properties['type'], - $properties['phpDocType'], - $properties['nativeType'], - $properties['passedByReference'], - $properties['variadic'], - $properties['defaultValue'], - $properties['outType'], - $properties['immediatelyInvokedCallable'], - $properties['closureThisType'], - ); + return $this->attributes; } } diff --git a/src/Reflection/Native/NativeFunctionReflection.php b/src/Reflection/Native/NativeFunctionReflection.php index ff52194908..7668d51f9e 100644 --- a/src/Reflection/Native/NativeFunctionReflection.php +++ b/src/Reflection/Native/NativeFunctionReflection.php @@ -3,12 +3,15 @@ namespace PHPStan\Reflection\Native; use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; +use function count; -class NativeFunctionReflection implements FunctionReflection +final class NativeFunctionReflection implements FunctionReflection { private Assertions $assertions; @@ -16,8 +19,9 @@ class NativeFunctionReflection implements FunctionReflection private TrinaryLogic $returnsByReference; /** - * @param ParametersAcceptorWithPhpDocs[] $variants - * @param ParametersAcceptorWithPhpDocs[]|null $namedArgumentsVariants + * @param list $variants + * @param list|null $namedArgumentsVariants + * @param list $attributes */ public function __construct( private string $name, @@ -26,9 +30,11 @@ public function __construct( private ?Type $throwType, private TrinaryLogic $hasSideEffects, private bool $isDeprecated, - ?Assertions $assertions = null, - private ?string $phpDocComment = null, - ?TrinaryLogic $returnsByReference = null, + ?Assertions $assertions, + private ?string $phpDocComment, + ?TrinaryLogic $returnsByReference, + private bool $acceptsNamedArguments, + private array $attributes, ) { $this->assertions = $assertions ?? Assertions::createEmpty(); @@ -45,14 +51,21 @@ public function getFileName(): ?string return null; } - /** - * @return ParametersAcceptorWithPhpDocs[] - */ public function getVariants(): array { return $this->variants; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + $variants = $this->getVariants(); + if (count($variants) !== 1) { + throw new ShouldNotHappenException(); + } + + return $variants[0]; + } + public function getNamedArgumentsVariants(): ?array { return $this->namedArgumentsVariants; @@ -78,11 +91,6 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isFinal(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - public function hasSideEffects(): TrinaryLogic { if ($this->isVoid()) { @@ -132,4 +140,14 @@ public function returnsByReference(): TrinaryLogic return $this->returnsByReference; } + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->acceptsNamedArguments); + } + + public function getAttributes(): array + { + return $this->attributes; + } + } diff --git a/src/Reflection/Native/NativeMethodReflection.php b/src/Reflection/Native/NativeMethodReflection.php index 3112fefa48..91fc46229d 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -2,38 +2,44 @@ namespace PHPStan\Reflection\Native; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\MethodPrototypeReflection; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; -use PHPStan\Reflection\Php\BuiltinMethodReflection; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; use ReflectionException; +use function count; use function strtolower; -class NativeMethodReflection implements ExtendedMethodReflection +final class NativeMethodReflection implements ExtendedMethodReflection { /** - * @param ParametersAcceptorWithPhpDocs[] $variants - * @param ParametersAcceptorWithPhpDocs[]|null $namedArgumentsVariants + * @param list $variants + * @param list|null $namedArgumentsVariants + * @param list $attributes */ public function __construct( private ReflectionProvider $reflectionProvider, private ClassReflection $declaringClass, - private BuiltinMethodReflection $reflection, + private ReflectionMethod $reflection, private array $variants, private ?array $namedArgumentsVariants, private TrinaryLogic $hasSideEffects, private ?Type $throwType, private Assertions $assertions, + private bool $acceptsNamedArguments, private ?Type $selfOutType, private ?string $phpDocComment, + private array $attributes, ) { } @@ -78,7 +84,7 @@ public function getPrototype(): ClassMemberReflection $tentativeReturnType = null; if ($prototypeMethod->getTentativeReturnType() !== null) { - $tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType()); + $tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType(), selfClass: $prototypeDeclaringClass); } return new MethodPrototypeReflection( @@ -88,7 +94,6 @@ public function getPrototype(): ClassMemberReflection $prototypeMethod->isPrivate(), $prototypeMethod->isPublic(), $prototypeMethod->isAbstract(), - $prototypeMethod->isFinal(), $prototypeMethod->isInternal(), $prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants(), $tentativeReturnType, @@ -108,6 +113,16 @@ 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; @@ -120,10 +135,15 @@ public function getDeprecatedDescription(): ?string public function isDeprecated(): TrinaryLogic { - return $this->reflection->isDeprecated(); + return TrinaryLogic::createFromBoolean($this->reflection->isDeprecated()); } public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBuiltin(): TrinaryLogic { return TrinaryLogic::createFromBoolean($this->reflection->isInternal()); } @@ -187,6 +207,11 @@ public function getAsserts(): Assertions return $this->assertions; } + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->declaringClass->acceptsNamedArguments() && $this->acceptsNamedArguments); + } + public function getSelfOutType(): ?Type { return $this->selfOutType; @@ -194,7 +219,12 @@ public function getSelfOutType(): ?Type public function returnsByReference(): TrinaryLogic { - return $this->reflection->returnsByReference(); + return TrinaryLogic::createFromBoolean($this->reflection->returnsReference()); + } + + public function getAttributes(): array + { + return $this->attributes; } } diff --git a/src/Reflection/Native/NativeParameterReflection.php b/src/Reflection/Native/NativeParameterReflection.php index 7f92bfdb5e..e812086830 100644 --- a/src/Reflection/Native/NativeParameterReflection.php +++ b/src/Reflection/Native/NativeParameterReflection.php @@ -7,7 +7,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class NativeParameterReflection implements ParameterReflection +final class NativeParameterReflection implements ParameterReflection { public function __construct( @@ -63,19 +63,4 @@ public function union(self $other): self ); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['name'], - $properties['optional'], - $properties['type'], - $properties['passedByReference'], - $properties['variadic'], - $properties['defaultValue'], - ); - } - } diff --git a/src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php b/src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php index d0b71b91a7..fc9b44f79a 100644 --- a/src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php +++ b/src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php @@ -11,9 +11,12 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -class NativeReflectionEnumReturnDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension +final class NativeReflectionEnumReturnDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { + /** + * @param class-string $className + */ public function __construct(private PhpVersion $phpVersion, private string $className, private string $methodName) { } diff --git a/src/Reflection/ParametersAcceptor.php b/src/Reflection/ParametersAcceptor.php index f4c2d4f1c0..b5fa5f1a2d 100644 --- a/src/Reflection/ParametersAcceptor.php +++ b/src/Reflection/ParametersAcceptor.php @@ -20,7 +20,7 @@ public function getTemplateTypeMap(): TemplateTypeMap; public function getResolvedTemplateTypeMap(): TemplateTypeMap; /** - * @return array + * @return list */ public function getParameters(): array; diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 33f536fe49..81bc3a2a99 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -9,6 +9,7 @@ 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; @@ -17,7 +18,7 @@ use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\Php\DummyParameter; -use PHPStan\Reflection\Php\DummyParameterWithPhpDocs; +use PHPStan\Reflection\Php\ExtendedDummyParameter; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -44,6 +45,7 @@ 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; @@ -53,32 +55,12 @@ use const ARRAY_FILTER_USE_KEY; use const CURLOPT_SSL_VERIFYHOST; -/** @api */ -class ParametersAcceptorSelector +/** + * @api + */ +final class ParametersAcceptorSelector { - /** - * @template T of ParametersAcceptor - * @param T[] $parametersAcceptors - * @return T - */ - public static function selectSingle( - array $parametersAcceptors, - ): ParametersAcceptor - { - $count = count($parametersAcceptors); - if ($count === 0) { - throw new ShouldNotHappenException( - 'getVariants() must return at least one variant.', - ); - } - if ($count !== 1) { - throw new ShouldNotHappenException('Multiple variants - use selectFromArgs() instead.'); - } - - return $parametersAcceptors[0]; - } - /** * @param Node\Arg[] $args * @param ParametersAcceptor[] $parametersAcceptors @@ -136,7 +118,7 @@ public static function selectFromArgs( $parameters, $acceptor->isVariadic(), $acceptor->getReturnType(), - $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ), ]; } @@ -163,10 +145,10 @@ public static function selectFromArgs( new FunctionVariant( $acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), - $parameters, + array_values($parameters), $acceptor->isVariadic(), $acceptor->getReturnType(), - $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ), ]; } @@ -213,10 +195,10 @@ public static function selectFromArgs( new FunctionVariant( $acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), - $parameters, + array_values($parameters), $acceptor->isVariadic(), $acceptor->getReturnType(), - $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ), ]; } @@ -244,10 +226,41 @@ public static function selectFromArgs( new FunctionVariant( $acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), - $parameters, + array_values($parameters), + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + + if (isset($args[0]) && (bool) $args[0]->getAttribute(ArrayFindArgVisitor::ATTRIBUTE_NAME)) { + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + $argType = $scope->getType($args[0]->value); + $parameters[1] = new NativeParameterReflection( + $parameters[1]->getName(), + $parameters[1]->isOptional(), + new CallableType( + [ + new DummyParameter('value', $scope->getIterableValueType($argType), false, PassedByReference::createNo(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($argType), false, PassedByReference::createNo(), false, null), + ], + new BooleanType(), + false, + ), + $parameters[1]->passedByReference(), + $parameters[1]->isVariadic(), + $parameters[1]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_values($parameters), $acceptor->isVariadic(), $acceptor->getReturnType(), - $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ), ]; } @@ -263,9 +276,8 @@ public static function selectFromArgs( if ((new ObjectType(Closure::class))->isSuperTypeOf($varType)->yes()) { $inFunction = $scope->getFunction(); if ($inFunction !== null) { - $inFunctionVariant = self::selectSingle($inFunction->getVariants()); $closureThisParameters = []; - foreach ($inFunctionVariant->getParameters() as $parameter) { + foreach ($inFunction->getParameters() as $parameter) { if ($parameter->getClosureThisType() === null) { continue; } @@ -290,7 +302,7 @@ public static function selectFromArgs( $parameters, $acceptor->isVariadic(), $acceptor->getReturnType(), - $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ), ]; } @@ -307,9 +319,8 @@ public static function selectFromArgs( $closureVarName = $args[0]->value->name; $inFunction = $scope->getFunction(); if ($inFunction !== null) { - $inFunctionVariant = self::selectSingle($inFunction->getVariants()); $closureThisParameters = []; - foreach ($inFunctionVariant->getParameters() as $parameter) { + foreach ($inFunction->getParameters() as $parameter) { if ($parameter->getClosureThisType() === null) { continue; } @@ -331,10 +342,10 @@ public static function selectFromArgs( new FunctionVariant( $acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), - $parameters, + array_values($parameters), $acceptor->isVariadic(), $acceptor->getReturnType(), - $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ), ]; } @@ -411,7 +422,7 @@ private static function hasAcceptorTemplateOrLateResolvableType(ParametersAccept foreach ($acceptor->getParameters() as $parameter) { if ( - $parameter instanceof ParameterReflectionWithPhpDocs + $parameter instanceof ExtendedParameterReflection && $parameter->getOutType() !== null && self::hasTemplateOrLateResolvableType($parameter->getOutType()) ) { @@ -419,7 +430,7 @@ private static function hasAcceptorTemplateOrLateResolvableType(ParametersAccept } if ( - $parameter instanceof ParameterReflectionWithPhpDocs + $parameter instanceof ExtendedParameterReflection && $parameter->getClosureThisType() !== null && self::hasTemplateOrLateResolvableType($parameter->getClosureThisType()) ) { @@ -531,7 +542,7 @@ public static function selectFromTypes( if ($parameter->getType() instanceof MixedType) { $isSuperType = $isSuperType->and(TrinaryLogic::createMaybe()); } else { - $isSuperType = $isSuperType->and($parameter->getType()->isSuperTypeOf($type)); + $isSuperType = $isSuperType->and($parameter->getType()->isSuperTypeOf($type)->result); } } @@ -563,7 +574,7 @@ public static function selectFromTypes( /** * @param ParametersAcceptor[] $acceptors */ - public static function combineAcceptors(array $acceptors): ParametersAcceptorWithPhpDocs + public static function combineAcceptors(array $acceptors): ExtendedParametersAcceptor { if (count($acceptors) === 0) { throw new ShouldNotHappenException( @@ -603,11 +614,12 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit $impurePoints = []; $invalidateExpressions = []; $usedVariables = []; + $acceptsNamedArguments = TrinaryLogic::createNo(); foreach ($acceptors as $acceptor) { $returnTypes[] = $acceptor->getReturnType(); - if ($acceptor instanceof ParametersAcceptorWithPhpDocs) { + if ($acceptor instanceof ExtendedParametersAcceptor) { $phpDocReturnTypes[] = $acceptor->getPhpDocReturnType(); $nativeReturnTypes[] = $acceptor->getNativeReturnType(); } @@ -618,23 +630,25 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit $impurePoints = array_merge($impurePoints, $acceptor->getImpurePoints()); $invalidateExpressions = array_merge($invalidateExpressions, $acceptor->getInvalidateExpressions()); $usedVariables = array_merge($usedVariables, $acceptor->getUsedVariables()); + $acceptsNamedArguments = $acceptsNamedArguments->or($acceptor->acceptsNamedArguments()); } $isVariadic = $isVariadic || $acceptor->isVariadic(); foreach ($acceptor->getParameters() as $i => $parameter) { if (!isset($parameters[$i])) { - $parameters[$i] = new DummyParameterWithPhpDocs( + $parameters[$i] = new ExtendedDummyParameter( $parameter->getName(), $parameter->getType(), $i + 1 > $minimumNumberOfParameters, $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue(), - $parameter instanceof ParameterReflectionWithPhpDocs ? $parameter->getNativeType() : new MixedType(), - $parameter instanceof ParameterReflectionWithPhpDocs ? $parameter->getPhpDocType() : new MixedType(), - $parameter instanceof ParameterReflectionWithPhpDocs ? $parameter->getOutType() : null, - $parameter instanceof ParameterReflectionWithPhpDocs ? $parameter->isImmediatelyInvokedCallable() : TrinaryLogic::createMaybe(), - $parameter instanceof ParameterReflectionWithPhpDocs ? $parameter->getClosureThisType() : null, + $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; } @@ -654,7 +668,8 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit $outType = $parameters[$i]->getOutType(); $immediatelyInvokedCallable = $parameters[$i]->isImmediatelyInvokedCallable(); $closureThisType = $parameters[$i]->getClosureThisType(); - if ($parameter instanceof ParameterReflectionWithPhpDocs) { + $attributes = $parameters[$i]->getAttributes(); + if ($parameter instanceof ExtendedParameterReflection) { $nativeType = TypeCombinator::union($nativeType, $parameter->getNativeType()); $phpDocType = TypeCombinator::union($phpDocType, $parameter->getPhpDocType()); @@ -671,6 +686,7 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit } $immediatelyInvokedCallable = $parameter->isImmediatelyInvokedCallable()->or($immediatelyInvokedCallable); + $attributes = array_merge($attributes, $parameter->getAttributes()); } else { $nativeType = new MixedType(); $phpDocType = $type; @@ -679,7 +695,7 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit $closureThisType = null; } - $parameters[$i] = new DummyParameterWithPhpDocs( + $parameters[$i] = new ExtendedDummyParameter( $parameters[$i]->getName() !== $parameter->getName() ? sprintf('%s|%s', $parameters[$i]->getName(), $parameter->getName()) : $parameter->getName(), $type, $i + 1 > $minimumNumberOfParameters, @@ -691,6 +707,7 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit $outType, $immediatelyInvokedCallable, $closureThisType, + $attributes, ); if ($isVariadic) { @@ -705,10 +722,10 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit $nativeReturnType = $nativeReturnTypes === [] ? null : TypeCombinator::union(...$nativeReturnTypes); if ($callableOccurred) { - return new CallableFunctionVariantWithPhpDocs( + return new ExtendedCallableFunctionVariant( TemplateTypeMap::createEmpty(), null, - $parameters, + array_values($parameters), $isVariadic, $returnType, $phpDocReturnType ?? $returnType, @@ -719,13 +736,14 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit $impurePoints, $invalidateExpressions, $usedVariables, + $acceptsNamedArguments, ); } - return new FunctionVariantWithPhpDocs( + return new ExtendedFunctionVariant( TemplateTypeMap::createEmpty(), null, - $parameters, + array_values($parameters), $isVariadic, $returnType, $phpDocReturnType ?? $returnType, @@ -733,17 +751,17 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit ); } - private static function wrapAcceptor(ParametersAcceptor $acceptor): ParametersAcceptorWithPhpDocs + private static function wrapAcceptor(ParametersAcceptor $acceptor): ExtendedParametersAcceptor { - if ($acceptor instanceof ParametersAcceptorWithPhpDocs) { + if ($acceptor instanceof ExtendedParametersAcceptor) { return $acceptor; } if ($acceptor instanceof CallableParametersAcceptor) { - return new CallableFunctionVariantWithPhpDocs( + return new ExtendedCallableFunctionVariant( $acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), - array_map(static fn (ParameterReflection $parameter): ParameterReflectionWithPhpDocs => self::wrapParameter($parameter), $acceptor->getParameters()), + array_map(static fn (ParameterReflection $parameter): ExtendedParameterReflection => self::wrapParameter($parameter), $acceptor->getParameters()), $acceptor->isVariadic(), $acceptor->getReturnType(), $acceptor->getReturnType(), @@ -754,13 +772,14 @@ private static function wrapAcceptor(ParametersAcceptor $acceptor): ParametersAc $acceptor->getImpurePoints(), $acceptor->getInvalidateExpressions(), $acceptor->getUsedVariables(), + $acceptor->acceptsNamedArguments(), ); } - return new FunctionVariantWithPhpDocs( + return new ExtendedFunctionVariant( $acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), - array_map(static fn (ParameterReflection $parameter): ParameterReflectionWithPhpDocs => self::wrapParameter($parameter), $acceptor->getParameters()), + array_map(static fn (ParameterReflection $parameter): ExtendedParameterReflection => self::wrapParameter($parameter), $acceptor->getParameters()), $acceptor->isVariadic(), $acceptor->getReturnType(), $acceptor->getReturnType(), @@ -769,9 +788,9 @@ private static function wrapAcceptor(ParametersAcceptor $acceptor): ParametersAc ); } - private static function wrapParameter(ParameterReflection $parameter): ParameterReflectionWithPhpDocs + private static function wrapParameter(ParameterReflection $parameter): ExtendedParameterReflection { - return $parameter instanceof ParameterReflectionWithPhpDocs ? $parameter : new DummyParameterWithPhpDocs( + return $parameter instanceof ExtendedParameterReflection ? $parameter : new ExtendedDummyParameter( $parameter->getName(), $parameter->getType(), $parameter->isOptional(), @@ -783,6 +802,7 @@ private static function wrapParameter(ParameterReflection $parameter): Parameter null, TrinaryLogic::createMaybe(), null, + [], ); } @@ -903,49 +923,90 @@ private static function getCurlOptValueType(int $curlOpt): ?Type } } + $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_CUSTOMREQUEST', 'CURLOPT_DEFAULT_PROTOCOL', - 'CURLOPT_DNS_INTERFACE', - 'CURLOPT_DNS_LOCAL_IP4', - 'CURLOPT_DNS_LOCAL_IP6', + 'CURLOPT_DNS_SERVERS', 'CURLOPT_EGDSOCKET', - 'CURLOPT_FTPPORT', + '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_PROXY_SERVICE_NAME', + '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_SSL_CIPHER_LIST', - 'CURLOPT_PROXY_TLS13_CIPHERS', '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_RANGE', + '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_PUBLIC_KEYFILE', + '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', @@ -955,13 +1016,14 @@ private static function getCurlOptValueType(int $curlOpt): ?Type 'CURLOPT_SSLKEYPASSWD', 'CURLOPT_SSLKEYTYPE', 'CURLOPT_TLS13_CIPHERS', - 'CURLOPT_UNIX_SOCKET_PATH', + 'CURLOPT_TLSAUTH_PASSWORD', + 'CURLOPT_TLSAUTH_TYPE', + 'CURLOPT_TLSAUTH_USERNAME', + 'CURLOPT_TRANSFER_ENCODING', 'CURLOPT_URL', 'CURLOPT_USERAGENT', 'CURLOPT_USERNAME', - 'CURLOPT_PASSWORD', 'CURLOPT_USERPWD', - 'CURLOPT_XOAUTH2_BEARER', ]; foreach ($nonEmptyStringConstants as $constName) { if (defined($constName) && constant($constName) === $curlOpt) { @@ -974,7 +1036,7 @@ private static function getCurlOptValueType(int $curlOpt): ?Type $stringConstants = [ 'CURLOPT_COOKIEFILE', - 'CURLOPT_ENCODING', + 'CURLOPT_ENCODING', // Alias: CURLOPT_ACCEPT_ENCODING 'CURLOPT_PRE_PROXY', 'CURLOPT_PRIVATE', 'CURLOPT_PROXY', diff --git a/src/Reflection/PassedByReference.php b/src/Reflection/PassedByReference.php index d4741bc1b6..804d049b43 100644 --- a/src/Reflection/PassedByReference.php +++ b/src/Reflection/PassedByReference.php @@ -4,8 +4,10 @@ use function array_key_exists; -/** @api */ -class PassedByReference +/** + * @api + */ +final class PassedByReference { private const NO = 1; @@ -74,12 +76,4 @@ public function combine(self $other): self return $this; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self - { - return new self($properties['value']); - } - } diff --git a/src/Reflection/Php/BuiltinMethodReflection.php b/src/Reflection/Php/BuiltinMethodReflection.php deleted file mode 100644 index d2d55336aa..0000000000 --- a/src/Reflection/Php/BuiltinMethodReflection.php +++ /dev/null @@ -1,60 +0,0 @@ -closureType->getTemplateTypeMap(), $this->closureType->getResolvedTemplateTypeMap(), - array_map(static fn (ParameterReflection $parameter): ParameterReflectionWithPhpDocs => new DummyParameterWithPhpDocs( + array_map(static fn (ParameterReflection $parameter): ExtendedParameterReflection => new ExtendedDummyParameter( $parameter->getName(), $parameter->getType(), $parameter->isOptional(), @@ -95,6 +96,7 @@ public function getVariants(): array null, TrinaryLogic::createMaybe(), null, + [], ), $parameters), $this->closureType->isVariadic(), $this->closureType->getReturnType(), @@ -105,6 +107,11 @@ public function getVariants(): array ]; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + public function getNamedArgumentsVariants(): ?array { return null; @@ -135,6 +142,16 @@ 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(); @@ -150,6 +167,11 @@ public function getAsserts(): Assertions return $this->nativeMethodReflection->getAsserts(); } + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->nativeMethodReflection->acceptsNamedArguments(); + } + public function getSelfOutType(): ?Type { return $this->nativeMethodReflection->getSelfOutType(); @@ -175,4 +197,9 @@ public function isPure(): TrinaryLogic return $this->nativeMethodReflection->isPure(); } + public function getAttributes(): array + { + return $this->nativeMethodReflection->getAttributes(); + } + } diff --git a/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php b/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php index bd3c9b54ed..abeb693630 100644 --- a/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php @@ -7,7 +7,7 @@ use PHPStan\Type\ClosureType; use PHPStan\Type\Type; -class ClosureCallUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection +final class ClosureCallUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { public function __construct(private UnresolvedMethodPrototypeReflection $prototype, private ClosureType $closure) diff --git a/src/Reflection/Php/EnumAllowedSubTypesClassReflectionExtension.php b/src/Reflection/Php/EnumAllowedSubTypesClassReflectionExtension.php index 3ba005da82..d36306a1b5 100644 --- a/src/Reflection/Php/EnumAllowedSubTypesClassReflectionExtension.php +++ b/src/Reflection/Php/EnumAllowedSubTypesClassReflectionExtension.php @@ -2,12 +2,14 @@ namespace PHPStan\Reflection\Php; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\AllowedSubTypesClassReflectionExtension; use PHPStan\Reflection\ClassReflection; use PHPStan\Type\Enum\EnumCaseObjectType; use function array_keys; -class EnumAllowedSubTypesClassReflectionExtension implements AllowedSubTypesClassReflectionExtension +#[AutowiredService] +final class EnumAllowedSubTypesClassReflectionExtension implements AllowedSubTypesClassReflectionExtension { public function supports(ClassReflection $classReflection): bool diff --git a/src/Reflection/Php/EnumCasesMethodReflection.php b/src/Reflection/Php/EnumCasesMethodReflection.php index 0665eee656..ecf72e435d 100644 --- a/src/Reflection/Php/EnumCasesMethodReflection.php +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -5,15 +5,16 @@ use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedFunctionVariant; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\MixedType; use PHPStan\Type\Type; -class EnumCasesMethodReflection implements ExtendedMethodReflection +final class EnumCasesMethodReflection implements ExtendedMethodReflection { public function __construct(private ClassReflection $declaringClass, private Type $returnType) @@ -63,7 +64,7 @@ public function getPrototype(): ClassMemberReflection public function getVariants(): array { return [ - new FunctionVariantWithPhpDocs( + new ExtendedFunctionVariant( TemplateTypeMap::createEmpty(), TemplateTypeMap::createEmpty(), [], @@ -75,6 +76,11 @@ public function getVariants(): array ]; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + public function getNamedArgumentsVariants(): ?array { return null; @@ -105,6 +111,11 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function getThrowType(): ?Type { return null; @@ -120,6 +131,11 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->declaringClass->acceptsNamedArguments()); + } + public function getSelfOutType(): ?Type { return null; @@ -140,4 +156,9 @@ public function isPure(): TrinaryLogic return TrinaryLogic::createYes(); } + public function getAttributes(): array + { + return []; + } + } diff --git a/src/Reflection/Php/EnumPropertyReflection.php b/src/Reflection/Php/EnumPropertyReflection.php index e3f7927dec..891491a1c5 100644 --- a/src/Reflection/Php/EnumPropertyReflection.php +++ b/src/Reflection/Php/EnumPropertyReflection.php @@ -3,17 +3,25 @@ namespace PHPStan\Reflection\Php; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; -class EnumPropertyReflection implements PropertyReflection +final class EnumPropertyReflection implements ExtendedPropertyReflection { - public function __construct(private ClassReflection $declaringClass, private Type $type) + public function __construct(private string $name, private ClassReflection $declaringClass, private Type $type) { } + public function getName(): string + { + return $this->name; + } + public function getDeclaringClass(): ClassReflection { return $this->declaringClass; @@ -39,6 +47,26 @@ public function getDocComment(): ?string return null; } + public function hasPhpDocType(): bool + { + return false; + } + + public function getPhpDocType(): Type + { + return new MixedType(); + } + + public function hasNativeType(): bool + { + return false; + } + + public function getNativeType(): Type + { + return new MixedType(); + } + public function getReadableType(): Type { return $this->type; @@ -79,4 +107,49 @@ 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 []; + } + } diff --git a/src/Reflection/Php/EnumUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Php/EnumUnresolvedPropertyPrototypeReflection.php index 5d76711ce4..5e5a048e7c 100644 --- a/src/Reflection/Php/EnumUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Php/EnumUnresolvedPropertyPrototypeReflection.php @@ -2,11 +2,11 @@ namespace PHPStan\Reflection\Php; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\Type\Type; -class EnumUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection +final class EnumUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { public function __construct(private EnumPropertyReflection $property) @@ -18,12 +18,12 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototy return $this; } - public function getNakedProperty(): PropertyReflection + public function getNakedProperty(): ExtendedPropertyReflection { return $this->property; } - public function getTransformedProperty(): PropertyReflection + public function getTransformedProperty(): ExtendedPropertyReflection { return $this->property; } diff --git a/src/Reflection/Php/ExitFunctionReflection.php b/src/Reflection/Php/ExitFunctionReflection.php new file mode 100644 index 0000000000..4020bbdc09 --- /dev/null +++ b/src/Reflection/Php/ExitFunctionReflection.php @@ -0,0 +1,146 @@ +name; + } + + public function getFileName(): ?string + { + return null; + } + + public function getVariants(): array + { + $parameterType = new UnionType([ + new StringType(), + new IntegerType(), + ]); + return [ + new ExtendedFunctionVariant( + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + [ + new ExtendedDummyParameter( + 'status', + $parameterType, + true, + PassedByReference::createNo(), + false, + new ConstantIntegerType(0), + $parameterType, + new MixedType(), + null, + TrinaryLogic::createNo(), + null, + [], + ), + ], + false, + new NeverType(true), + new MixedType(), + new NeverType(true), + TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + + /** + * @return list + */ + public function getNamedArgumentsVariants(): array + { + return $this->getVariants(); + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getThrowType(): ?Type + { + return null; + } + + public function hasSideEffects(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isBuiltin(): bool + { + return true; + } + + public function getAsserts(): Assertions + { + return Assertions::createEmpty(); + } + + public function getDocComment(): ?string + { + return null; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getAttributes(): array + { + return []; + } + +} diff --git a/src/Reflection/Php/DummyParameterWithPhpDocs.php b/src/Reflection/Php/ExtendedDummyParameter.php similarity index 66% rename from src/Reflection/Php/DummyParameterWithPhpDocs.php rename to src/Reflection/Php/ExtendedDummyParameter.php index 262b601bea..19a917e0a1 100644 --- a/src/Reflection/Php/DummyParameterWithPhpDocs.php +++ b/src/Reflection/Php/ExtendedDummyParameter.php @@ -2,14 +2,19 @@ namespace PHPStan\Reflection\Php; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; +use PHPStan\Reflection\AttributeReflection; +use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; -class DummyParameterWithPhpDocs extends DummyParameter implements ParameterReflectionWithPhpDocs +final class ExtendedDummyParameter extends DummyParameter implements ExtendedParameterReflection { + /** + * @param list $attributes + */ public function __construct( string $name, Type $type, @@ -22,6 +27,7 @@ public function __construct( private ?Type $outType, private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, + private array $attributes, ) { parent::__construct($name, $type, $optional, $passedByReference, $variadic, $defaultValue); @@ -32,6 +38,11 @@ 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; @@ -52,4 +63,9 @@ public function getClosureThisType(): ?Type return $this->closureThisType; } + public function getAttributes(): array + { + return $this->attributes; + } + } diff --git a/src/Reflection/Php/NativeBuiltinMethodReflection.php b/src/Reflection/Php/NativeBuiltinMethodReflection.php deleted file mode 100644 index 2f10d431c5..0000000000 --- a/src/Reflection/Php/NativeBuiltinMethodReflection.php +++ /dev/null @@ -1,149 +0,0 @@ -reflection->getName(); - } - - public function getReflection(): ?ReflectionMethod - { - return $this->reflection; - } - - public function getFileName(): ?string - { - $fileName = $this->reflection->getFileName(); - if ($fileName === false) { - return null; - } - - return $fileName; - } - - public function getDeclaringClass(): ReflectionClass|ReflectionEnum - { - 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(): ReflectionIntersectionType|ReflectionNamedType|ReflectionUnionType|null - { - return $this->reflection->getReturnType(); - } - - public function getTentativeReturnType(): ReflectionIntersectionType|ReflectionNamedType|ReflectionUnionType|null - { - return $this->reflection->getTentativeReturnType(); - } - - /** - * @return ReflectionParameter[] - */ - public function getParameters(): array - { - return $this->reflection->getParameters(); - } - - public function returnsByReference(): TrinaryLogic - { - return TrinaryLogic::createFromBoolean($this->reflection->returnsReference()); - } - -} diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index f3ff0aa2ab..939e1550d3 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -10,6 +10,7 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\ScopeContext; use PHPStan\Analyser\ScopeFactory; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty; use PHPStan\Parser\Parser; @@ -19,15 +20,18 @@ use PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension; use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension; use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflectionFactory; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\Deprecation\DeprecationProvider; +use PHPStan\Reflection\ExtendedFunctionVariant; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; +use PHPStan\Reflection\Native\ExtendedNativeParameterReflection; use PHPStan\Reflection\Native\NativeMethodReflection; -use PHPStan\Reflection\Native\NativeParameterWithPhpDocsReflection; use PHPStan\Reflection\PropertiesClassReflectionExtension; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\SignatureMap\FunctionSignature; use PHPStan\Reflection\SignatureMap\ParameterSignature; @@ -41,6 +45,7 @@ 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; @@ -60,11 +65,11 @@ use function sprintf; use function strtolower; -class PhpClassReflectionExtension +final class PhpClassReflectionExtension implements PropertiesClassReflectionExtension, MethodsClassReflectionExtension { - /** @var PropertyReflection[][] */ + /** @var ExtendedPropertyReflection[][] */ private array $propertiesIncludingAnnotations = []; /** @var PhpPropertyReflection[][] */ @@ -87,6 +92,7 @@ public function __construct( private NodeScopeResolver $nodeScopeResolver, private PhpMethodReflectionFactory $methodReflectionFactory, private PhpDocInheritanceResolver $phpDocInheritanceResolver, + private DeprecationProvider $deprecationProvider, private AnnotationsMethodsClassReflectionExtension $annotationsMethodsClassReflectionExtension, private AnnotationsPropertiesClassReflectionExtension $annotationsPropertiesClassReflectionExtension, private SignatureMapProvider $signatureMapProvider, @@ -94,6 +100,7 @@ public function __construct( private StubPhpDocProvider $stubPhpDocProvider, private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, private FileTypeMapper $fileTypeMapper, + private AttributeReflectionFactory $attributeReflectionFactory, private bool $inferPrivatePropertyTypeFromConstructor, ) { @@ -152,7 +159,7 @@ public function hasProperty(ClassReflection $classReflection, string $propertyNa return $classReflection->getNativeReflection()->hasProperty($propertyName); } - public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection + public function getProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection { if (!isset($this->propertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName])) { $this->propertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName] = $this->createProperty($classReflection, $propertyName, true); @@ -176,7 +183,7 @@ private function createProperty( ClassReflection $classReflection, string $propertyName, bool $includingAnnotations, - ): PropertyReflection + ): ExtendedPropertyReflection { $propertyReflection = $classReflection->getNativeReflection()->getProperty($propertyName); $propertyName = $propertyReflection->getName(); @@ -211,14 +218,16 @@ private function createProperty( $types[] = $value; } - return new PhpPropertyReflection($declaringClassReflection, null, null, TypeCombinator::union(...$types), $classReflection->getNativeReflection()->getProperty($propertyName), null, false, false, false, false); + return new PhpPropertyReflection($declaringClassReflection, null, null, TypeCombinator::union(...$types), $classReflection->getNativeReflection()->getProperty($propertyName), null, null, null, false, false, false, false, [], false); } } - $deprecatedDescription = null; - $isDeprecated = false; + $deprecation = $this->deprecationProvider->getPropertyDeprecation($propertyReflection); + $deprecatedDescription = $deprecation === null ? null : $deprecation->getDescription(); + $isDeprecated = $deprecation !== null; $isInternal = false; $isReadOnlyByPhpDoc = $classReflection->isImmutable(); + $isFinal = $classReflection->isFinal() || $propertyReflection->isFinal(); $isAllowedPrivateMutation = false; if ( @@ -293,10 +302,14 @@ private function createProperty( $phpDocBlockClassReflection->getCallSiteVarianceMap(), TemplateTypeVariance::createInvariant(), ) : null; - $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; - $isDeprecated = $resolvedPhpDoc->isDeprecated(); + + if (!$isDeprecated) { + $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; + $isDeprecated = $resolvedPhpDoc->isDeprecated(); + } $isInternal = $resolvedPhpDoc->isInternal(); $isReadOnlyByPhpDoc = $isReadOnlyByPhpDoc || $resolvedPhpDoc->isReadOnly(); + $isFinal = $isFinal || $resolvedPhpDoc->isFinal(); $isAllowedPrivateMutation = $resolvedPhpDoc->isAllowedPrivateMutation(); } @@ -352,17 +365,79 @@ private function createProperty( $declaringTrait = $reflectionProvider->getClass($declaringTraitName); } + $getHook = null; + $setHook = null; + + $betterReflection = $propertyReflection->getBetterReflection(); + if ($betterReflection->hasHook('get')) { + $betterReflectionGetHook = $betterReflection->getHook('get'); + if ($betterReflectionGetHook === null) { + throw new ShouldNotHappenException(); + } + $getHook = $this->createUserlandMethodReflection( + $declaringClassReflection, + $declaringClassReflection, + new ReflectionMethod($betterReflectionGetHook), + $declaringTraitName, + ); + + if ($phpDocType !== null) { + $getHookMethodReflectionVariant = $getHook->getOnlyVariant(); + $getHookMethodReflectionVariantPhpDocReturnType = $getHookMethodReflectionVariant->getPhpDocReturnType(); + if ( + $getHookMethodReflectionVariantPhpDocReturnType instanceof MixedType + && !$getHookMethodReflectionVariantPhpDocReturnType instanceof TemplateMixedType + && !$getHookMethodReflectionVariantPhpDocReturnType->isExplicitMixed() + ) { + $getHook = $getHook->changePropertyGetHookPhpDocType($phpDocType); + } + } + } + + if ($betterReflection->hasHook('set')) { + $betterReflectionSetHook = $betterReflection->getHook('set'); + if ($betterReflectionSetHook === null) { + throw new ShouldNotHappenException(); + } + $setHook = $this->createUserlandMethodReflection( + $declaringClassReflection, + $declaringClassReflection, + new ReflectionMethod($betterReflectionSetHook), + $declaringTraitName, + ); + + if ($phpDocType !== null) { + $setHookMethodReflectionVariant = $setHook->getOnlyVariant(); + $setHookMethodReflectionParameters = $setHookMethodReflectionVariant->getParameters(); + if (isset($setHookMethodReflectionParameters[0])) { + $setHookMethodReflectionParameter = $setHookMethodReflectionParameters[0]; + $setHookMethodReflectionParameterPhpDocType = $setHookMethodReflectionParameter->getPhpDocType(); + if ( + $setHookMethodReflectionParameterPhpDocType instanceof MixedType + && !$setHookMethodReflectionParameterPhpDocType instanceof TemplateMixedType + && !$setHookMethodReflectionParameterPhpDocType->isExplicitMixed() + ) { + $setHook = $setHook->changePropertySetHookPhpDocType($setHookMethodReflectionParameter->getName(), $phpDocType); + } + } + } + } + return new PhpPropertyReflection( $declaringClassReflection, $declaringTrait, $nativeType, $phpDocType, $propertyReflection, + $getHook, + $setHook, $deprecatedDescription, $isDeprecated, $isInternal, $isReadOnlyByPhpDoc, $isAllowedPrivateMutation, + $this->attributeReflectionFactory->fromNativeReflection($propertyReflection->getAttributes(), InitializerExprContext::fromClass($declaringClassReflection->getName(), $declaringClassReflection->getFileName())), + $isFinal, ); } @@ -371,16 +446,13 @@ public function hasMethod(ClassReflection $classReflection, string $methodName): return $classReflection->getNativeReflection()->hasMethod($methodName); } - /** - * @return ExtendedMethodReflection - */ - 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; @@ -407,8 +479,7 @@ public function getNativeMethod(ClassReflection $classReflection, string $method throw new ShouldNotHappenException(); } - $reflectionMethod = $classReflection->getNativeReflection()->getMethod($methodName); - $nativeMethodReflection = new NativeBuiltinMethodReflection($reflectionMethod); + $nativeMethodReflection = $classReflection->getNativeReflection()->getMethod($methodName); if (!isset($this->nativeMethods[$classReflection->getCacheKey()][$nativeMethodReflection->getName()])) { $method = $this->createMethod($classReflection, $nativeMethodReflection, false); @@ -420,7 +491,7 @@ public function getNativeMethod(ClassReflection $classReflection, string $method private function createMethod( ClassReflection $classReflection, - BuiltinMethodReflection $methodReflection, + ReflectionMethod $methodReflection, bool $includingAnnotations, ): ExtendedMethodReflection { @@ -470,15 +541,12 @@ private function createMethod( if ($this->signatureMapProvider->hasMethodSignature($declaringClassName, $methodReflection->getName())) { $variantsByType = ['positional' => []]; - $reflectionMethod = null; $throwType = null; $asserts = Assertions::createEmpty(); + $acceptsNamedArguments = true; $selfOutType = null; $phpDocComment = null; - if ($classReflection->getNativeReflection()->hasMethod($methodReflection->getName())) { - $reflectionMethod = $classReflection->getNativeReflection()->getMethod($methodReflection->getName()); - } - $methodSignaturesResult = $this->signatureMapProvider->getMethodSignatures($declaringClassName, $methodReflection->getName(), $reflectionMethod); + $methodSignaturesResult = $this->signatureMapProvider->getMethodSignatures($declaringClassName, $methodReflection->getName(), $methodReflection); foreach ($methodSignaturesResult as $signatureType => $methodSignatures) { if ($methodSignatures === null) { continue; @@ -535,6 +603,7 @@ private function createMethod( } $asserts = Assertions::createFromResolvedPhpDocBlock($stubPhpDoc); + $acceptsNamedArguments = $stubPhpDoc->acceptsNamedArguments(); $selfOutTypeTag = $stubPhpDoc->getSelfOutTag(); if ($selfOutTypeTag !== null) { @@ -555,15 +624,15 @@ private function createMethod( } } } - if ($stubPhpDocPair === null && $reflectionMethod !== null && $reflectionMethod->getDocComment() !== false) { - $filename = $reflectionMethod->getFileName(); + if ($stubPhpDocPair === null && $methodReflection->getDocComment() !== false) { + $filename = $methodReflection->getFileName(); if ($filename !== false) { $phpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc( $filename, $declaringClassName, null, - $reflectionMethod->getName(), - $reflectionMethod->getDocComment(), + $methodReflection->getName(), + $methodReflection->getDocComment(), ); $throwsTag = $phpDocBlock->getThrowsTag(); if ($throwsTag !== null) { @@ -579,6 +648,7 @@ private function createMethod( $phpDocParameterTypes[$name] = $paramTag->getType(); } $asserts = Assertions::createFromResolvedPhpDocBlock($phpDocBlock); + $acceptsNamedArguments = $phpDocBlock->acceptsNamedArguments(); $selfOutTypeTag = $phpDocBlock->getSelfOutTag(); if ($selfOutTypeTag !== null) { @@ -594,7 +664,7 @@ private function createMethod( } $signatureParameters = $methodSignature->getParameters(); - foreach ($reflectionMethod->getParameters() as $paramI => $reflectionParameter) { + foreach ($methodReflection->getParameters() as $paramI => $reflectionParameter) { if (!array_key_exists($paramI, $signatureParameters)) { continue; } @@ -621,8 +691,10 @@ private function createMethod( $hasSideEffects, $throwType, $asserts, + $acceptsNamedArguments, $selfOutType, $phpDocComment, + $this->attributeReflectionFactory->fromNativeReflection($methodReflection->getAttributes(), InitializerExprContext::fromClassMethod($declaringClassName, null, $methodReflection->getName(), null)), ); } @@ -634,27 +706,29 @@ private function createMethod( ); } - public function createUserlandMethodReflection(ClassReflection $fileDeclaringClass, ClassReflection $actualDeclaringClass, BuiltinMethodReflection $methodReflection, ?string $declaringTraitName): PhpMethodReflection + 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($fileDeclaringClass, $fileDeclaringClass, $methodReflection->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters())); $phpDocBlockClassReflection = $fileDeclaringClass; - if ($methodReflection->getReflection() !== null) { - $methodDeclaringClass = $methodReflection->getReflection()->getBetterReflection()->getDeclaringClass(); - - 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(), - ), - ); - } + $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(), + ), + ); } } @@ -663,13 +737,13 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla } if ($resolvedPhpDoc === null) { - $docComment = $methodReflection->getDocComment(); + $docComment = $methodReflection->getDocComment() !== false ? $methodReflection->getDocComment() : null; $positionalParameterNames = array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters()); $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( $docComment, - $fileDeclaringClass->getFileName(), - $fileDeclaringClass, + $actualDeclaringClass->getFileName(), + $actualDeclaringClass, $declaringTraitName, $methodReflection->getName(), $positionalParameterNames, @@ -686,10 +760,7 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla } $phpDocParameterTypes = []; - if ( - $methodReflection instanceof NativeBuiltinMethodReflection - && $methodReflection->isConstructor() - ) { + if ($methodReflection->isConstructor()) { foreach ($methodReflection->getParameters() as $parameter) { if (!$parameter->isPromoted()) { continue; @@ -758,17 +829,29 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $nativeReturnType = TypehintHelper::decideTypeFromReflection( $methodReflection->getReturnType(), - null, - $actualDeclaringClass, + selfClass: $actualDeclaringClass, ); $phpDocReturnType = $this->getPhpDocReturnType($phpDocBlockClassReflection, $resolvedPhpDoc, $nativeReturnType); $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null; - $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; - $isDeprecated = $resolvedPhpDoc->isDeprecated(); + if (!$isDeprecated) { + $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; + $isDeprecated = $resolvedPhpDoc->isDeprecated(); + } $isInternal = $resolvedPhpDoc->isInternal(); $isFinal = $resolvedPhpDoc->isFinal(); - $isPure = $resolvedPhpDoc->isPure(); + $isPure = null; + foreach ($actualDeclaringClass->getAncestors() as $className => $ancestor) { + 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()) { @@ -794,6 +877,8 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $phpDocParameterOutTypes, $immediatelyInvokedCallableParameters, $closureThisParameters, + $acceptsNamedArguments, + $this->attributeReflectionFactory->fromNativeReflection($methodReflection->getAttributes(), InitializerExprContext::fromClassMethod($actualDeclaringClass->getName(), $declaringTraitName, $methodReflection->getName(), $actualDeclaringClass->getFileName())), ); } @@ -824,7 +909,7 @@ private function createNativeMethodVariant( array $stubClosureThisParameters, array $closureThisParameters, bool $usePhpDocParameterNames, - ): FunctionVariantWithPhpDocs + ): ExtendedFunctionVariant { $parameters = []; foreach ($methodSignature->getParameters() as $parameterSignature) { @@ -839,6 +924,7 @@ 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()])) { @@ -862,7 +948,7 @@ private function createNativeMethodVariant( $closureThisType = $closureThisParameters[$phpDocParameterName]; } - $parameters[] = new NativeParameterWithPhpDocsReflection( + $parameters[] = new ExtendedNativeParameterReflection( $usePhpDocParameterNames ? $phpDocParameterName : $parameterSignature->getName(), @@ -876,6 +962,7 @@ private function createNativeMethodVariant( $parameterOutType ?? $parameterSignature->getOutType(), $immediatelyInvoked, $closureThisType, + [], ); } @@ -886,7 +973,7 @@ private function createNativeMethodVariant( $returnType = TypehintHelper::decideType($methodSignature->getReturnType(), $phpDocReturnType); } - return new FunctionVariantWithPhpDocs( + return new ExtendedFunctionVariant( TemplateTypeMap::createEmpty(), null, $parameters, @@ -912,14 +999,10 @@ private function findPropertyTrait(ReflectionProperty $propertyReflection): ?str } private function findMethodTrait( - BuiltinMethodReflection $methodReflection, + ReflectionMethod $methodReflection, ): ?string { - if ($methodReflection->getReflection() === null) { - return null; - } - - $declaringClass = $methodReflection->getReflection()->getBetterReflection()->getDeclaringClass(); + $declaringClass = $methodReflection->getBetterReflection()->getDeclaringClass(); if ($declaringClass->isTrait()) { if ($methodReflection->getDeclaringClass()->isTrait() && $declaringClass->getName() === $methodReflection->getDeclaringClass()->getName()) { return null; diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index 8e2429016c..d49cb57f4a 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -8,14 +8,16 @@ use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflection; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\PassedByReference; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; @@ -26,23 +28,25 @@ /** * @api */ -class PhpFunctionFromParserNodeReflection implements FunctionReflection +class PhpFunctionFromParserNodeReflection implements FunctionReflection, ExtendedParametersAcceptor { - /** @var Function_|ClassMethod */ + /** @var Function_|ClassMethod|Node\PropertyHook */ private Node\FunctionLike $functionLike; - /** @var FunctionVariantWithPhpDocs[]|null */ + /** @var list|null */ private ?array $variants = null; /** - * @param Function_|ClassMethod $functionLike + * @param Function_|ClassMethod|Node\PropertyHook $functionLike * @param Type[] $realParameterTypes * @param Type[] $phpDocParameterTypes * @param Type[] $realParameterDefaultValues + * @param array> $parameterAttributes * @param Type[] $parameterOutTypes * @param array $immediatelyInvokedCallableParameters * @param array $phpDocClosureThisTypeParameters + * @param list $attributes */ public function __construct( FunctionLike $functionLike, @@ -51,13 +55,13 @@ public function __construct( private array $realParameterTypes, private array $phpDocParameterTypes, private array $realParameterDefaultValues, + private array $parameterAttributes, private Type $realReturnType, private ?Type $phpDocReturnType, private ?Type $throwType, private ?string $deprecatedDescription, private bool $isDeprecated, private bool $isInternal, - private bool $isFinal, protected ?bool $isPure, private bool $acceptsNamedArguments, private Assertions $assertions, @@ -65,6 +69,7 @@ public function __construct( private array $parameterOutTypes, private array $immediatelyInvokedCallableParameters, private array $phpDocClosureThisTypeParameters, + private array $attributes, ) { $this->functionLike = $functionLike; @@ -86,6 +91,11 @@ public function getName(): string return $this->functionLike->name->name; } + if (!$this->functionLike instanceof Function_) { + // PropertyHook is handled in PhpMethodFromParserNodeReflection subclass + throw new ShouldNotHappenException(); + } + if ($this->functionLike->namespacedName === null) { throw new ShouldNotHappenException(); } @@ -93,26 +103,24 @@ public function getName(): string return (string) $this->functionLike->namespacedName; } - /** - * @return ParametersAcceptorWithPhpDocs[] - */ public function getVariants(): array { - if ($this->variants === null) { - $this->variants = [ - new FunctionVariantWithPhpDocs( - $this->templateTypeMap, - null, - $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(), + ), + ]; + } - return $this->variants; + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this; } public function getNamedArgumentsVariants(): ?array @@ -120,10 +128,20 @@ public function getNamedArgumentsVariants(): ?array return null; } + public function getTemplateTypeMap(): TemplateTypeMap + { + return $this->templateTypeMap; + } + + public function getResolvedTemplateTypeMap(): TemplateTypeMap + { + return TemplateTypeMap::createEmpty(); + } + /** - * @return ParameterReflectionWithPhpDocs[] + * @return list */ - private function getParameters(): array + public function getParameters(): array { $parameters = []; $isOptional = true; @@ -163,13 +181,14 @@ private function getParameters(): array $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) { @@ -180,11 +199,26 @@ private function isVariadic(): bool return false; } - protected 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) { @@ -204,24 +238,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 isFinalByKeyword(): TrinaryLogic - { - $finalMethod = false; - if ($this->functionLike instanceof ClassMethod) { - $finalMethod = $this->functionLike->isFinal(); - } - return TrinaryLogic::createFromBoolean($finalMethod); - } - public function getThrowType(): ?Type { return $this->throwType; @@ -249,9 +265,9 @@ public function isGenerator(): bool return $this->nodeIsOrContainsYield($this->functionLike); } - public function acceptsNamedArguments(): bool + public function acceptsNamedArguments(): TrinaryLogic { - return $this->acceptsNamedArguments; + return TrinaryLogic::createFromBoolean($this->acceptsNamedArguments); } private function nodeIsOrContainsYield(Node $node): bool @@ -309,4 +325,9 @@ public function isPure(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->isPure); } + public function getAttributes(): array + { + return $this->attributes; + } + } diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index d437c86f5e..38604e8761 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -2,23 +2,23 @@ namespace PHPStan\Reflection\Php; -use PhpParser\Node; -use PhpParser\Node\Stmt\ClassLike; -use PhpParser\Node\Stmt\Declare_; -use PhpParser\Node\Stmt\Function_; -use PhpParser\Node\Stmt\Namespace_; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; -use PHPStan\Cache\Cache; -use PHPStan\Parser\FunctionCallStatementFinder; +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\FunctionReflectionFactory; +use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\MixedType; @@ -26,29 +26,32 @@ use PHPStan\Type\TypehintHelper; use function array_key_exists; use function array_map; -use function filemtime; +use function count; +use function is_array; use function is_file; -use function sprintf; -use function time; -class PhpFunctionReflection implements FunctionReflection +#[GenerateFactory(interface: FunctionReflectionFactory::class)] +final class PhpFunctionReflection implements FunctionReflection { - /** @var FunctionVariantWithPhpDocs[]|null */ + /** @var list|null */ private ?array $variants = null; + private ?bool $containsVariadicCalls = null; + /** * @param array $phpDocParameterTypes * @param array $phpDocParameterOutTypes * @param array $phpDocParameterImmediatelyInvokedCallable * @param array $phpDocParameterClosureThisTypes + * @param list $attributes */ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ReflectionFunction $reflection, + #[AutowiredParameter(ref: '@defaultAnalysisParser')] private Parser $parser, - private FunctionCallStatementFinder $functionCallStatementFinder, - private Cache $cache, + private AttributeReflectionFactory $attributeReflectionFactory, private TemplateTypeMap $templateTypeMap, private array $phpDocParameterTypes, private ?Type $phpDocReturnType, @@ -56,14 +59,15 @@ public function __construct( private ?string $deprecatedDescription, private bool $isDeprecated, private bool $isInternal, - private bool $isFinal, private ?string $filename, private ?bool $isPure, private Assertions $asserts, + private bool $acceptsNamedArguments, private ?string $phpDocComment, private array $phpDocParameterOutTypes, private array $phpDocParameterImmediatelyInvokedCallable, private array $phpDocParameterClosureThisTypes, + private array $attributes, ) { } @@ -86,26 +90,24 @@ public function getFileName(): ?string 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 @@ -114,7 +116,7 @@ public function getNamedArgumentsVariants(): ?array } /** - * @return ParameterReflectionWithPhpDocs[] + * @return list */ private function getParameters(): array { @@ -132,6 +134,7 @@ private function getParameters(): array $this->phpDocParameterOutTypes[$reflection->getName()] ?? null, $immediatelyInvokedCallable, $this->phpDocParameterClosureThisTypes[$reflection->getName()] ?? null, + $this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), InitializerExprContext::fromReflectionParameter($reflection)), ); }, $this->reflection->getParameters()); } @@ -139,68 +142,33 @@ private function getParameters(): array private function isVariadic(): bool { $isNativelyVariadic = $this->reflection->isVariadic(); - if (!$isNativelyVariadic && $this->reflection - ->getFileName() !== false) { - $fileName = $this->reflection->getFileName(); - if (is_file($fileName)) { - $functionName = $this->reflection->getName(); - $modifiedTime = filemtime($fileName); - if ($modifiedTime === false) { - $modifiedTime = time(); - } - $variableCacheKey = sprintf('%d-v3', $modifiedTime); - $key = sprintf('variadic-function-%s-%s', $functionName, $fileName); - $cachedResult = $this->cache->load($key, $variableCacheKey); - if ($cachedResult === null) { - $nodes = $this->parser->parseFile($fileName); - $result = $this->callsFuncGetArgs($nodes); - $this->cache->save($key, $variableCacheKey, $result); - return $result; - } + if (!$isNativelyVariadic && $this->reflection->getFileName() !== false && !$this->isBuiltin()) { + $filename = $this->reflection->getFileName(); - return $cachedResult; + if ($this->containsVariadicCalls !== null) { + return $this->containsVariadicCalls; } - } - return $isNativelyVariadic; - } - - /** - * @param Node[] $nodes - */ - 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 @@ -231,6 +199,11 @@ public function getDeprecatedDescription(): ?string return $this->deprecatedDescription; } + if ($this->reflection->isDeprecated()) { + $attributes = $this->reflection->getBetterReflection()->getAttributes(); + return DeprecatedAttributeHelper::getDeprecatedDescription($attributes); + } + return null; } @@ -246,11 +219,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; @@ -297,4 +265,14 @@ public function returnsByReference(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->reflection->returnsReference()); } + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->acceptsNamedArguments); + } + + public function getAttributes(): array + { + return $this->attributes; + } + } diff --git a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php index 7e858b714e..cd0e6fcbf6 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -5,10 +5,12 @@ 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\ExtendedMethodReflection; use PHPStan\Reflection\MissingMethodFromReflectionException; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; @@ -21,36 +23,41 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\VoidType; use function in_array; +use function sprintf; use function strtolower; /** * @api */ -class PhpMethodFromParserNodeReflection extends PhpFunctionFromParserNodeReflection implements ExtendedMethodReflection +final class PhpMethodFromParserNodeReflection extends PhpFunctionFromParserNodeReflection implements ExtendedMethodReflection { /** * @param Type[] $realParameterTypes * @param Type[] $phpDocParameterTypes * @param Type[] $realParameterDefaultValues + * @param array> $parameterAttributes * @param array $immediatelyInvokedCallableParameters * @param array $phpDocClosureThisTypeParameters + * @param list $attributes */ public function __construct( private ClassReflection $declaringClass, - ClassMethod $classMethod, + private ClassMethod|Node\PropertyHook $classMethod, + private ?string $hookForProperty, string $fileName, TemplateTypeMap $templateTypeMap, array $realParameterTypes, array $phpDocParameterTypes, array $realParameterDefaultValues, + array $parameterAttributes, Type $realReturnType, ?Type $phpDocReturnType, ?Type $throwType, ?string $deprecatedDescription, bool $isDeprecated, bool $isInternal, - bool $isFinal, + private bool $isFinal, ?bool $isPure, bool $acceptsNamedArguments, Assertions $assertions, @@ -59,10 +66,23 @@ public function __construct( 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 (in_array($name, ['__construct', '__destruct', '__unset', '__wakeup', '__clone'], true)) { + if ($this->isConstructor) { + $realReturnType = new VoidType(); + } + if (in_array($name, ['__destruct', '__unset', '__wakeup', '__clone'], true)) { $realReturnType = new VoidType(); } if ($name === '__tostring') { @@ -101,13 +121,13 @@ public function __construct( $realParameterTypes, $phpDocParameterTypes, $realParameterDefaultValues, + $parameterAttributes, $realReturnType, $phpDocReturnType, $throwType, $deprecatedDescription, $isDeprecated, $isInternal, - $isFinal || $classMethod->isFinal(), $isPure, $acceptsNamedArguments, $assertions, @@ -115,6 +135,7 @@ public function __construct( $parameterOutTypes, $immediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, + $attributes, ); } @@ -132,26 +153,102 @@ public function getPrototype(): ClassMemberReflection } } - private function getClassMethod(): ClassMethod + private function getClassMethod(): ClassMethod|Node\PropertyHook { - /** @var Node\Stmt\ClassMethod $functionLike */ + /** @var Node\Stmt\ClassMethod|Node\PropertyHook $functionLike */ $functionLike = $this->getFunctionLike(); return $functionLike; } + public function getName(): string + { + $function = $this->getFunctionLike(); + if (!$function instanceof Node\PropertyHook) { + return parent::getName(); + } + + if ($this->hookForProperty === null) { + throw new ShouldNotHappenException('Hook was provided but property was not'); + } + + return sprintf('$%s::%s', $this->hookForProperty, $function->name->toString()); + } + + /** + * @phpstan-assert-if-true !null $this->getHookedPropertyName() + * @phpstan-assert-if-true !null $this->getPropertyHookName() + */ + public function isPropertyHook(): bool + { + return $this->hookForProperty !== null; + } + + public function getHookedPropertyName(): ?string + { + return $this->hookForProperty; + } + + /** + * @return 'get'|'set'|null + */ + public function getPropertyHookName(): ?string + { + $function = $this->getFunctionLike(); + if (!$function instanceof Node\PropertyHook) { + return null; + } + + $name = $function->name->toLowerString(); + if (!in_array($name, ['get', 'set'], true)) { + throw new ShouldNotHappenException(sprintf('Unknown property hook: %s', $name)); + } + + return $name; + } + public function isStatic(): bool { - return $this->getClassMethod()->isStatic(); + $method = $this->getClassMethod(); + if ($method instanceof Node\PropertyHook) { + return false; + } + + return $method->isStatic(); } public function isPrivate(): bool { - return $this->getClassMethod()->isPrivate(); + $method = $this->getClassMethod(); + if ($method instanceof Node\PropertyHook) { + return false; + } + + return $method->isPrivate(); } public function isPublic(): bool { - return $this->getClassMethod()->isPublic(); + $method = $this->getClassMethod(); + if ($method instanceof Node\PropertyHook) { + return true; + } + + return $method->isPublic(); + } + + public function isFinal(): TrinaryLogic + { + $method = $this->getClassMethod(); + if ($method instanceof Node\PropertyHook) { + return TrinaryLogic::createFromBoolean($method->isFinal()); + } + + return TrinaryLogic::createFromBoolean($method->isFinal() || $this->isFinal); + } + + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->getClassMethod()->isFinal()); } public function isBuiltin(): bool @@ -171,7 +268,17 @@ public function returnsByReference(): TrinaryLogic public function isAbstract(): TrinaryLogic { - return TrinaryLogic::createFromBoolean($this->getClassMethod()->isAbstract()); + $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 diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index 0fba6c34ce..22982ca1f3 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -2,25 +2,25 @@ namespace PHPStan\Reflection\Php; -use PhpParser\Node; -use PhpParser\Node\Stmt\ClassMethod; -use PhpParser\Node\Stmt\Declare_; -use PhpParser\Node\Stmt\Function_; -use PhpParser\Node\Stmt\Namespace_; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; -use PHPStan\Cache\Cache; -use PHPStan\Parser\FunctionCallStatementFinder; +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\ExtendedFunctionVariant; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; +use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\MethodPrototypeReflection; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\ReflectionProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; @@ -35,45 +35,51 @@ use PHPStan\Type\TypehintHelper; use PHPStan\Type\VoidType; use ReflectionException; +use function array_key_exists; use function array_map; +use function count; use function explode; -use function filemtime; use function in_array; -use function is_bool; +use function is_array; use function sprintf; use function strtolower; -use function time; use const PHP_VERSION_ID; -/** @api */ -class PhpMethodReflection implements ExtendedMethodReflection +/** + * @api + */ +#[GenerateFactory(interface: PhpMethodReflectionFactory::class)] +final class PhpMethodReflection implements ExtendedMethodReflection { - /** @var PhpParameterReflection[]|null */ + /** @var list|null */ private ?array $parameters = null; private ?Type $returnType = null; private ?Type $nativeReturnType = null; - /** @var FunctionVariantWithPhpDocs[]|null */ + /** @var list|null */ private ?array $variants = null; + private ?bool $containsVariadicCalls = null; + /** * @param Type[] $phpDocParameterTypes * @param Type[] $phpDocParameterOutTypes * @param array $immediatelyInvokedCallableParameters * @param array $phpDocClosureThisTypeParameters + * @param list $attributes */ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ClassReflection $declaringClass, private ?ClassReflection $declaringTrait, - private BuiltinMethodReflection $reflection, + private ReflectionMethod $reflection, private ReflectionProvider $reflectionProvider, + private AttributeReflectionFactory $attributeReflectionFactory, + #[AutowiredParameter(ref: '@defaultAnalysisParser')] private Parser $parser, - private FunctionCallStatementFinder $functionCallStatementFinder, - private Cache $cache, private TemplateTypeMap $templateTypeMap, private array $phpDocParameterTypes, private ?Type $phpDocReturnType, @@ -84,11 +90,13 @@ public function __construct( 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, ) { } @@ -121,7 +129,7 @@ public function getPrototype(): ClassMemberReflection $tentativeReturnType = null; if ($prototypeMethod->getTentativeReturnType() !== null) { - $tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType()); + $tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType(), selfClass: $prototypeDeclaringClass); } return new MethodPrototypeReflection( @@ -131,7 +139,6 @@ public function getPrototype(): ClassMemberReflection $prototypeMethod->isPrivate(), $prototypeMethod->isPublic(), $prototypeMethod->isAbstract(), - $prototypeMethod->isFinal(), $prototypeMethod->isInternal(), $prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants(), $tentativeReturnType, @@ -187,25 +194,26 @@ 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 @@ -214,23 +222,20 @@ public function getNamedArgumentsVariants(): ?array } /** - * @return ParameterReflectionWithPhpDocs[] + * @return list */ private function getParameters(): array { - if ($this->parameters === null) { - $this->parameters = array_map(fn (ReflectionParameter $reflection): PhpParameterReflection => new PhpParameterReflection( - $this->initializerExprTypeResolver, - $reflection, - $this->phpDocParameterTypes[$reflection->getName()] ?? null, - $this->getDeclaringClass()->getName(), - $this->phpDocParameterOutTypes[$reflection->getName()] ?? null, - $this->immediatelyInvokedCallableParameters[$reflection->getName()] ?? TrinaryLogic::createMaybe(), - $this->phpDocClosureThisTypeParameters[$reflection->getName()] ?? null, - ), $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 @@ -243,82 +248,40 @@ private function isVariadic(): bool $filename = $this->declaringTrait->getFileName(); } - if (!$isNativelyVariadic && $filename !== null) { - $modifiedTime = @filemtime($filename); - if ($modifiedTime === false) { - $modifiedTime = time(); - } - $key = sprintf('variadic-method-%s-%s-%s', $declaringClass->getName(), $this->reflection->getName(), $filename); - $variableCacheKey = sprintf('%d-v4', $modifiedTime); - $cachedResult = $this->cache->load($key, $variableCacheKey); - if ($cachedResult === null || !is_bool($cachedResult)) { - $nodes = $this->parser->parseFile($filename); - $result = $this->callsFuncGetArgs($declaringClass, $nodes); - $this->cache->save($key, $variableCacheKey, $result); - return $result; + if (!$isNativelyVariadic && $filename !== null && !$this->declaringClass->isBuiltin()) { + if ($this->containsVariadicCalls !== null) { + return $this->containsVariadicCalls; } - return $cachedResult; - } - - return $isNativelyVariadic; - } - - /** - * @param Node[] $nodes - */ - private function callsFuncGetArgs(ClassReflection $declaringClass, array $nodes): bool - { - foreach ($nodes as $node) { - if ( - $node instanceof Node\Stmt\ClassLike - ) { - if (!isset($node->namespacedName)) { - continue; - } - if ($declaringClass->getName() !== (string) $node->namespacedName) { - continue; - } - if ($this->callsFuncGetArgs($declaringClass, $node->stmts)) { - return true; - } - continue; + $className = $declaringClass->getName(); + if ($declaringClass->isAnonymous()) { + $className = sprintf('%s:%s:%s', VariadicMethodsVisitor::ANONYMOUS_CLASS_PREFIX, $declaringClass->getNativeReflection()->getStartLine(), $declaringClass->getNativeReflection()->getEndLine()); } - - if ($node instanceof ClassMethod) { - if ($node->getStmts() === null) { - continue; // interface - } - - $methodName = $node->name->name; - if ($methodName === $this->reflection->getName()) { - return $this->functionCallStatementFinder->findFunctionCallInStatements(ParametersAcceptor::VARIADIC_FUNCTIONS, $node->getStmts()) !== null; + if (array_key_exists($className, VariadicMethodsVisitor::$cache)) { + if (array_key_exists($this->reflection->getName(), VariadicMethodsVisitor::$cache[$className])) { + return $this->containsVariadicCalls = VariadicMethodsVisitor::$cache[$className][$this->reflection->getName()]; } - continue; + return $this->containsVariadicCalls = false; } - if ($node instanceof Function_) { - continue; - } + $nodes = $this->parser->parseFile($filename); + if (count($nodes) > 0) { + $variadicMethods = $nodes[0]->getAttribute(VariadicMethodsVisitor::ATTRIBUTE_NAME); - if ($node instanceof Namespace_) { - if ($this->callsFuncGetArgs($declaringClass, $node->stmts)) { - return true; + if ( + is_array($variadicMethods) + && array_key_exists($className, $variadicMethods) + && array_key_exists($this->reflection->getName(), $variadicMethods[$className]) + ) { + return $this->containsVariadicCalls = $variadicMethods[$className][$this->reflection->getName()]; } - continue; - } - - if (!$node instanceof Declare_ || $node->stmts === null) { - continue; } - if ($this->callsFuncGetArgs($declaringClass, $node->stmts)) { - return true; - } + return $this->containsVariadicCalls = false; } - return false; + return $isNativelyVariadic; } public function isPrivate(): bool @@ -375,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, - ); - } - - return $this->nativeReturnType; + return $this->nativeReturnType ??= TypehintHelper::decideTypeFromReflection( + $this->reflection->getReturnType(), + selfClass: $this->declaringClass, + ); } public function getDeprecatedDescription(): ?string @@ -392,6 +350,11 @@ public function getDeprecatedDescription(): ?string return $this->deprecatedDescription; } + if ($this->reflection->isDeprecated()) { + $attributes = $this->reflection->getBetterReflection()->getAttributes(); + return DeprecatedAttributeHelper::getDeprecatedDescription($attributes); + } + return null; } @@ -401,12 +364,17 @@ public function isDeprecated(): TrinaryLogic return TrinaryLogic::createYes(); } - return $this->reflection->isDeprecated(); + return TrinaryLogic::createFromBoolean($this->reflection->isDeprecated()); } public function isInternal(): TrinaryLogic { - return TrinaryLogic::createFromBoolean($this->isInternal || $this->reflection->isInternal()); + return TrinaryLogic::createFromBoolean($this->isInternal); + } + + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->isInternal()); } public function isFinal(): TrinaryLogic @@ -453,6 +421,13 @@ 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; @@ -465,7 +440,7 @@ public function getDocComment(): ?string public function returnsByReference(): TrinaryLogic { - return $this->reflection->returnsByReference(); + return TrinaryLogic::createFromBoolean($this->reflection->returnsReference()); } public function isPure(): TrinaryLogic @@ -477,4 +452,72 @@ public function isPure(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->isPure); } + public function changePropertyGetHookPhpDocType(Type $phpDocType): self + { + return new self( + $this->initializerExprTypeResolver, + $this->declaringClass, + $this->declaringTrait, + $this->reflection, + $this->reflectionProvider, + $this->attributeReflectionFactory, + $this->parser, + $this->templateTypeMap, + $this->phpDocParameterTypes, + $phpDocType, + $this->phpDocThrowType, + $this->deprecatedDescription, + $this->isDeprecated, + $this->isInternal, + $this->isFinal, + $this->isPure, + $this->asserts, + $this->acceptsNamedArguments, + $this->selfOutType, + $this->phpDocComment, + $this->phpDocParameterOutTypes, + $this->immediatelyInvokedCallableParameters, + $this->phpDocClosureThisTypeParameters, + $this->attributes, + ); + } + + public function changePropertySetHookPhpDocType(string $parameterName, Type $phpDocType): self + { + $phpDocParameterTypes = $this->phpDocParameterTypes; + $phpDocParameterTypes[$parameterName] = $phpDocType; + + return new self( + $this->initializerExprTypeResolver, + $this->declaringClass, + $this->declaringTrait, + $this->reflection, + $this->reflectionProvider, + $this->attributeReflectionFactory, + $this->parser, + $this->templateTypeMap, + $phpDocParameterTypes, + $this->phpDocReturnType, + $this->phpDocThrowType, + $this->deprecatedDescription, + $this->isDeprecated, + $this->isInternal, + $this->isFinal, + $this->isPure, + $this->asserts, + $this->acceptsNamedArguments, + $this->selfOutType, + $this->phpDocComment, + $this->phpDocParameterOutTypes, + $this->immediatelyInvokedCallableParameters, + $this->phpDocClosureThisTypeParameters, + $this->attributes, + ); + } + + public function getAttributes(): array + { + return $this->attributes; + } + } diff --git a/src/Reflection/Php/PhpMethodReflectionFactory.php b/src/Reflection/Php/PhpMethodReflectionFactory.php index 8da0dcb5c6..ec95a2de81 100644 --- a/src/Reflection/Php/PhpMethodReflectionFactory.php +++ b/src/Reflection/Php/PhpMethodReflectionFactory.php @@ -2,7 +2,9 @@ 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; @@ -16,11 +18,12 @@ interface PhpMethodReflectionFactory * @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, @@ -34,8 +37,10 @@ public function create( ?Type $selfOutType, ?string $phpDocComment, array $phpDocParameterOutTypes, - array $immediatelyInvokedCallableParameters = [], - array $phpDocClosureThisTypeParameters = [], + array $immediatelyInvokedCallableParameters, + array $phpDocClosureThisTypeParameters, + bool $acceptsNamedArguments, + array $attributes, ): PhpMethodReflection; } diff --git a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php index 9dd5a25958..f048ea7100 100644 --- a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php @@ -2,7 +2,8 @@ namespace PHPStan\Reflection\Php; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; +use PHPStan\Reflection\AttributeReflection; +use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; @@ -10,11 +11,14 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; -class PhpParameterFromParserNodeReflection implements ParameterReflectionWithPhpDocs +final class PhpParameterFromParserNodeReflection implements ExtendedParameterReflection { private ?Type $type = null; + /** + * @param list $attributes + */ public function __construct( private string $name, private bool $optional, @@ -26,6 +30,7 @@ public function __construct( private ?Type $outType, private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, + private array $attributes, ) { } @@ -63,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; @@ -98,4 +108,9 @@ 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 0e6ce5ae77..8469f7bef4 100644 --- a/src/Reflection/Php/PhpParameterReflection.php +++ b/src/Reflection/Php/PhpParameterReflection.php @@ -3,9 +3,11 @@ namespace PHPStan\Reflection\Php; 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\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; @@ -13,21 +15,25 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; -class PhpParameterReflection implements ParameterReflectionWithPhpDocs +final class PhpParameterReflection implements ExtendedParameterReflection { private ?Type $type = null; private ?Type $nativeType = null; + /** + * @param list $attributes + */ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ReflectionParameter $reflection, private ?Type $phpDocType, - private ?string $declaringClassName, + private ?ClassReflection $declaringClass, private ?Type $outType, private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, + private array $attributes, ) { } @@ -62,7 +68,7 @@ public function getType(): Type $this->type = TypehintHelper::decideTypeFromReflection( $this->reflection->getType(), $phpDocType, - $this->declaringClassName, + $this->declaringClass, $this->isVariadic(), ); } @@ -91,18 +97,18 @@ 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 @@ -132,4 +138,9 @@ 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 00fc7e3e8e..2e7a4eb41e 100644 --- a/src/Reflection/Php/PhpPropertyReflection.php +++ b/src/Reflection/Php/PhpPropertyReflection.php @@ -6,36 +6,54 @@ use PHPStan\BetterReflection\Reflection\Adapter\ReflectionNamedType; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionUnionType; +use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\MissingMethodFromReflectionException; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; +use function sprintf; -/** @api */ -class PhpPropertyReflection implements PropertyReflection +/** + * @api + */ +final class PhpPropertyReflection implements ExtendedPropertyReflection { private ?Type $finalNativeType = null; private ?Type $type = null; + /** + * @param list $attributes + */ public function __construct( private ClassReflection $declaringClass, private ?ClassReflection $declaringTrait, private ReflectionUnionType|ReflectionNamedType|ReflectionIntersectionType|null $nativeType, private ?Type $phpDocType, private ReflectionProperty $reflection, + private ?ExtendedMethodReflection $getHook, + private ?ExtendedMethodReflection $setHook, private ?string $deprecatedDescription, private bool $isDeprecated, private bool $isInternal, private bool $isReadOnlyByPhpDoc, private bool $isAllowedPrivateMutation, + private array $attributes, + private bool $isFinal, ) { } + public function getName(): string + { + return $this->reflection->getName(); + } + public function getDeclaringClass(): ClassReflection { return $this->declaringClass; @@ -83,24 +101,44 @@ public function isReadOnlyByPhpDoc(): bool public function getReadableType(): Type { - if ($this->type === null) { - $this->type = TypehintHelper::decideTypeFromReflection( - $this->nativeType, - $this->phpDocType, - $this->declaringClass, - ); - } - - return $this->type; + return $this->type ??= TypehintHelper::decideTypeFromReflection( + $this->nativeType, + $this->phpDocType, + $this->declaringClass, + ); } public function getWritableType(): Type { + if ($this->hasHook('set')) { + $setHookVariant = $this->getHook('set')->getOnlyVariant(); + $parameters = $setHookVariant->getParameters(); + if (isset($parameters[0])) { + return $parameters[0]->getType(); + } + } + return $this->getReadableType(); } public function canChangeTypeAfterAssignment(): bool { + if ($this->isStatic()) { + return true; + } + + if ($this->isVirtual()->yes()) { + return false; + } + + if ($this->hasHook('get')) { + return false; + } + + if ($this->hasHook('set')) { + return false; + } + return true; } @@ -130,25 +168,36 @@ public function hasNativeType(): bool public function getNativeType(): Type { - if ($this->finalNativeType === null) { - $this->finalNativeType = TypehintHelper::decideTypeFromReflection( - $this->nativeType, - null, - $this->declaringClass, - ); - } - - return $this->finalNativeType; + return $this->finalNativeType ??= TypehintHelper::decideTypeFromReflection( + $this->nativeType, + selfClass: $this->declaringClass, + ); } public function isReadable(): bool { - return true; + if ($this->isStatic()) { + return true; + } + + if (!$this->isVirtual()->yes()) { + return true; + } + + return $this->hasHook('get'); } public function isWritable(): bool { - return true; + if ($this->isStatic()) { + return true; + } + + if (!$this->isVirtual()->yes()) { + return true; + } + + return $this->hasHook('set'); } public function getDeprecatedDescription(): ?string @@ -180,4 +229,70 @@ 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; + } + } 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 9668d523be..6fb9316fb5 100644 --- a/src/Reflection/Php/SimpleXMLElementProperty.php +++ b/src/Reflection/Php/SimpleXMLElementProperty.php @@ -3,25 +3,34 @@ namespace PHPStan\Reflection\Php; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\BooleanType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; +use PHPStan\Type\MixedType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class SimpleXMLElementProperty implements PropertyReflection +final class SimpleXMLElementProperty implements ExtendedPropertyReflection { public function __construct( + private string $name, private ClassReflection $declaringClass, private Type $type, ) { } + public function getName(): string + { + return $this->name; + } + public function getDeclaringClass(): ClassReflection { return $this->declaringClass; @@ -42,6 +51,26 @@ public function isPublic(): bool return true; } + public function hasPhpDocType(): bool + { + return false; + } + + public function getPhpDocType(): Type + { + return new MixedType(); + } + + public function hasNativeType(): bool + { + return false; + } + + public function getNativeType(): Type + { + return new MixedType(); + } + public function getReadableType(): Type { return $this->type; @@ -93,4 +122,49 @@ 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 []; + } + } diff --git a/src/Reflection/Php/Soap/SoapClientMethodReflection.php b/src/Reflection/Php/Soap/SoapClientMethodReflection.php index 15a814b20a..1017888c59 100644 --- a/src/Reflection/Php/Soap/SoapClientMethodReflection.php +++ b/src/Reflection/Php/Soap/SoapClientMethodReflection.php @@ -12,7 +12,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -class SoapClientMethodReflection implements MethodReflection +final class SoapClientMethodReflection implements MethodReflection { public function __construct(private ClassReflection $declaringClass, private string $name) @@ -87,7 +87,7 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createNo(); } - public function getThrowType(): ?Type + public function getThrowType(): Type { return new ObjectType('SoapFault'); } diff --git a/src/Reflection/Php/Soap/SoapClientMethodsClassReflectionExtension.php b/src/Reflection/Php/Soap/SoapClientMethodsClassReflectionExtension.php index 73924b6081..431026f938 100644 --- a/src/Reflection/Php/Soap/SoapClientMethodsClassReflectionExtension.php +++ b/src/Reflection/Php/Soap/SoapClientMethodsClassReflectionExtension.php @@ -6,12 +6,12 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; -class SoapClientMethodsClassReflectionExtension implements MethodsClassReflectionExtension +final class SoapClientMethodsClassReflectionExtension implements MethodsClassReflectionExtension { public function hasMethod(ClassReflection $classReflection, string $methodName): bool { - return $classReflection->getName() === 'SoapClient' || $classReflection->isSubclassOf('SoapClient'); + return $classReflection->is('SoapClient'); } public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection diff --git a/src/Reflection/Php/UniversalObjectCrateProperty.php b/src/Reflection/Php/UniversalObjectCrateProperty.php index 3d766f5fee..924b249e6f 100644 --- a/src/Reflection/Php/UniversalObjectCrateProperty.php +++ b/src/Reflection/Php/UniversalObjectCrateProperty.php @@ -3,14 +3,18 @@ namespace PHPStan\Reflection\Php; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; -class UniversalObjectCrateProperty implements PropertyReflection +final class UniversalObjectCrateProperty implements ExtendedPropertyReflection { public function __construct( + private string $name, private ClassReflection $declaringClass, private Type $readableType, private Type $writableType, @@ -18,6 +22,11 @@ public function __construct( { } + public function getName(): string + { + return $this->name; + } + public function getDeclaringClass(): ClassReflection { return $this->declaringClass; @@ -38,6 +47,26 @@ public function isPublic(): bool return true; } + public function hasPhpDocType(): bool + { + return false; + } + + public function getPhpDocType(): Type + { + return new MixedType(); + } + + public function hasNativeType(): bool + { + return false; + } + + public function getNativeType(): Type + { + return new MixedType(); + } + public function getReadableType(): Type { return $this->readableType; @@ -83,4 +112,49 @@ 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 []; + } + } diff --git a/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php b/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php index a5aaa8443f..0cd79eb232 100644 --- a/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php +++ b/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php @@ -4,18 +4,17 @@ use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\PropertiesClassReflectionExtension; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\MixedType; -class UniversalObjectCratesClassReflectionExtension +final class UniversalObjectCratesClassReflectionExtension implements PropertiesClassReflectionExtension { /** - * @param string[] $classes + * @param list $classes */ public function __construct( private ReflectionProvider $reflectionProvider, @@ -27,17 +26,29 @@ public function __construct( public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { - return self::isUniversalObjectCrate( + return self::isUniversalObjectCrateImplementation( $this->reflectionProvider, $this->classes, $classReflection, ); } + public static function isUniversalObjectCrate( + ReflectionProvider $reflectionProvider, + ClassReflection $classReflection, + ): bool + { + return self::isUniversalObjectCrateImplementation( + $reflectionProvider, + $reflectionProvider->getUniversalObjectCratesClasses(), + $classReflection, + ); + } + /** - * @param string[] $classes + * @param list $classes */ - public static function isUniversalObjectCrate( + private static function isUniversalObjectCrateImplementation( ReflectionProvider $reflectionProvider, array $classes, ClassReflection $classReflection, @@ -48,10 +59,7 @@ public static function isUniversalObjectCrate( continue; } - if ( - $classReflection->getName() === $className - || $classReflection->isSubclassOf($className) - ) { + if ($classReflection->is($className)) { return true; } } @@ -66,18 +74,18 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa } 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 index 909c357874..363109b7c0 100644 --- a/src/Reflection/PhpVersionStaticAccessor.php +++ b/src/Reflection/PhpVersionStaticAccessor.php @@ -5,7 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\ShouldNotHappenException; -class PhpVersionStaticAccessor +final class PhpVersionStaticAccessor { private static ?PhpVersion $instance = null; diff --git a/src/Reflection/RealClassClassConstantReflection.php b/src/Reflection/RealClassClassConstantReflection.php new file mode 100644 index 0000000000..96795f6427 --- /dev/null +++ b/src/Reflection/RealClassClassConstantReflection.php @@ -0,0 +1,156 @@ + $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 e268800f5a..3b7387f85a 100644 --- a/src/Reflection/ReflectionProvider.php +++ b/src/Reflection/ReflectionProvider.php @@ -16,13 +16,14 @@ public function getClass(string $className): ClassReflection; public function getClassName(string $className): string; - public function supportsAnonymousClasses(): bool; - public function getAnonymousClassReflection( Node\Stmt\Class_ $classNode, Scope $scope, ): ClassReflection; + /** @return list */ + public function getUniversalObjectCratesClasses(): array; + public function hasFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool; public function getFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): FunctionReflection; @@ -31,7 +32,7 @@ public function resolveFunctionName(Node\Name $nameNode, ?NamespaceAnswerer $nam public function hasConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool; - public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): GlobalConstantReflection; + public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ConstantReflection; public function resolveConstantName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string; diff --git a/src/Reflection/ReflectionProvider/DirectReflectionProviderProvider.php b/src/Reflection/ReflectionProvider/DirectReflectionProviderProvider.php index e1c7e31e1f..2fccc5b3c4 100644 --- a/src/Reflection/ReflectionProvider/DirectReflectionProviderProvider.php +++ b/src/Reflection/ReflectionProvider/DirectReflectionProviderProvider.php @@ -4,7 +4,7 @@ use PHPStan\Reflection\ReflectionProvider; -class DirectReflectionProviderProvider implements ReflectionProviderProvider +final class DirectReflectionProviderProvider implements ReflectionProviderProvider { public function __construct(private ReflectionProvider $reflectionProvider) diff --git a/src/Reflection/ReflectionProvider/DummyReflectionProvider.php b/src/Reflection/ReflectionProvider/DummyReflectionProvider.php index ff3e666cff..7d18639f8c 100644 --- a/src/Reflection/ReflectionProvider/DummyReflectionProvider.php +++ b/src/Reflection/ReflectionProvider/DummyReflectionProvider.php @@ -5,13 +5,13 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\GlobalConstantReflection; use PHPStan\Reflection\NamespaceAnswerer; use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; -class DummyReflectionProvider implements ReflectionProvider +final class DummyReflectionProvider implements ReflectionProvider { public function hasClass(string $className): bool @@ -29,14 +29,14 @@ public function getClassName(string $className): string return $className; } - public function supportsAnonymousClasses(): bool + public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection { - return false; + throw new ShouldNotHappenException(); } - public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection + public function getUniversalObjectCratesClasses(): array { - throw new ShouldNotHappenException(); + return []; } public function hasFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool @@ -59,7 +59,7 @@ public function hasConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAn return false; } - public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): GlobalConstantReflection + public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ConstantReflection { throw new ShouldNotHappenException(); } diff --git a/src/Reflection/ReflectionProvider/LazyReflectionProviderProvider.php b/src/Reflection/ReflectionProvider/LazyReflectionProviderProvider.php index 9e4b1957a0..b6695b972c 100644 --- a/src/Reflection/ReflectionProvider/LazyReflectionProviderProvider.php +++ b/src/Reflection/ReflectionProvider/LazyReflectionProviderProvider.php @@ -2,10 +2,12 @@ namespace PHPStan\Reflection\ReflectionProvider; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; use PHPStan\Reflection\ReflectionProvider; -class LazyReflectionProviderProvider implements ReflectionProviderProvider +#[AutowiredService(as: ReflectionProviderProvider::class)] +final class LazyReflectionProviderProvider implements ReflectionProviderProvider { public function __construct(private Container $container) diff --git a/src/Reflection/ReflectionProvider/MemoizingReflectionProvider.php b/src/Reflection/ReflectionProvider/MemoizingReflectionProvider.php index b4059b7623..00f4301c7e 100644 --- a/src/Reflection/ReflectionProvider/MemoizingReflectionProvider.php +++ b/src/Reflection/ReflectionProvider/MemoizingReflectionProvider.php @@ -5,13 +5,13 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\GlobalConstantReflection; use PHPStan\Reflection\NamespaceAnswerer; use PHPStan\Reflection\ReflectionProvider; use function strtolower; -class MemoizingReflectionProvider implements ReflectionProvider +final class MemoizingReflectionProvider implements ReflectionProvider { /** @var array */ @@ -56,14 +56,14 @@ public function getClassName(string $className): string return $this->classNames[$lowerClassName] = $this->provider->getClassName($className); } - public function supportsAnonymousClasses(): bool + public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection { - return $this->provider->supportsAnonymousClasses(); + return $this->provider->getAnonymousClassReflection($classNode, $scope); } - public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection + public function getUniversalObjectCratesClasses(): array { - return $this->provider->getAnonymousClassReflection($classNode, $scope); + return $this->provider->getUniversalObjectCratesClasses(); } public function hasFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool @@ -86,7 +86,7 @@ public function hasConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAn return $this->provider->hasConstant($nameNode, $namespaceAnswerer); } - public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): GlobalConstantReflection + public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ConstantReflection { return $this->provider->getConstant($nameNode, $namespaceAnswerer); } diff --git a/src/Reflection/ReflectionProvider/ReflectionProviderFactory.php b/src/Reflection/ReflectionProvider/ReflectionProviderFactory.php index 7c9501cbdd..9f2e6424fd 100644 --- a/src/Reflection/ReflectionProvider/ReflectionProviderFactory.php +++ b/src/Reflection/ReflectionProvider/ReflectionProviderFactory.php @@ -2,12 +2,16 @@ namespace PHPStan\Reflection\ReflectionProvider; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ReflectionProvider; -class ReflectionProviderFactory +#[AutowiredService(name: 'reflectionProviderFactory')] +final class ReflectionProviderFactory { public function __construct( + #[AutowiredParameter(ref: '@betterReflectionProvider')] private ReflectionProvider $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 bb772064c7..90ad0dd185 100644 --- a/src/Reflection/ReflectionProviderStaticAccessor.php +++ b/src/Reflection/ReflectionProviderStaticAccessor.php @@ -4,7 +4,7 @@ use PHPStan\ShouldNotHappenException; -class ReflectionProviderStaticAccessor +final class ReflectionProviderStaticAccessor { private static ?ReflectionProvider $instance = null; diff --git a/src/Reflection/RequireExtension/RequireExtendsMethodsClassReflectionExtension.php b/src/Reflection/RequireExtension/RequireExtendsMethodsClassReflectionExtension.php index 920c84e7d4..ee6ada6f6f 100644 --- a/src/Reflection/RequireExtension/RequireExtendsMethodsClassReflectionExtension.php +++ b/src/Reflection/RequireExtension/RequireExtendsMethodsClassReflectionExtension.php @@ -5,11 +5,10 @@ use PHPStan\Analyser\OutOfClassScope; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; use PHPStan\ShouldNotHappenException; -class RequireExtendsMethodsClassReflectionExtension implements MethodsClassReflectionExtension +final class RequireExtendsMethodsClassReflectionExtension implements MethodsClassReflectionExtension { public function hasMethod(ClassReflection $classReflection, string $methodName): bool @@ -17,10 +16,7 @@ public function hasMethod(ClassReflection $classReflection, string $methodName): return $this->findMethod($classReflection, $methodName) !== null; } - /** - * @return ExtendedMethodReflection - */ - public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection + public function getMethod(ClassReflection $classReflection, string $methodName): ExtendedMethodReflection { $method = $this->findMethod($classReflection, $methodName); if ($method === null) { @@ -30,10 +26,7 @@ public function getMethod(ClassReflection $classReflection, string $methodName): return $method; } - /** - * @return ExtendedMethodReflection|null - */ - private function findMethod(ClassReflection $classReflection, string $methodName): ?MethodReflection + private function findMethod(ClassReflection $classReflection, string $methodName): ?ExtendedMethodReflection { if (!$classReflection->isInterface()) { return null; diff --git a/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php b/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php index ecaa6d3109..550a7bee59 100644 --- a/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php +++ b/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php @@ -4,11 +4,11 @@ use PHPStan\Analyser\OutOfClassScope; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\PropertiesClassReflectionExtension; -use PHPStan\Reflection\PropertyReflection; use PHPStan\ShouldNotHappenException; -class RequireExtendsPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension +final class RequireExtendsPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension { public function hasProperty(ClassReflection $classReflection, string $propertyName): bool @@ -16,7 +16,7 @@ public function hasProperty(ClassReflection $classReflection, string $propertyNa return $this->findProperty($classReflection, $propertyName) !== null; } - public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection + public function getProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection { $property = $this->findProperty($classReflection, $propertyName); if ($property === null) { @@ -26,7 +26,7 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa return $property; } - private function findProperty(ClassReflection $classReflection, string $propertyName): ?PropertyReflection + private function findProperty(ClassReflection $classReflection, string $propertyName): ?ExtendedPropertyReflection { if (!$classReflection->isInterface()) { return null; diff --git a/src/Reflection/ResolvedFunctionVariant.php b/src/Reflection/ResolvedFunctionVariant.php index a6139853fb..92675f4f19 100644 --- a/src/Reflection/ResolvedFunctionVariant.php +++ b/src/Reflection/ResolvedFunctionVariant.php @@ -4,13 +4,11 @@ use PHPStan\Type\Type; -interface ResolvedFunctionVariant extends ParametersAcceptorWithPhpDocs +interface ResolvedFunctionVariant extends ExtendedParametersAcceptor { public function getOriginalParametersAcceptor(): ParametersAcceptor; public function getReturnTypeWithUnresolvableTemplateTypes(): Type; - public function getPhpDocReturnTypeWithUnresolvableTemplateTypes(): Type; - } diff --git a/src/Reflection/ResolvedFunctionVariantWithCallable.php b/src/Reflection/ResolvedFunctionVariantWithCallable.php index 4714738167..59c9cf1bec 100644 --- a/src/Reflection/ResolvedFunctionVariantWithCallable.php +++ b/src/Reflection/ResolvedFunctionVariantWithCallable.php @@ -11,7 +11,7 @@ use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; -class ResolvedFunctionVariantWithCallable implements ResolvedFunctionVariant, CallableParametersAcceptor +final class ResolvedFunctionVariantWithCallable implements ResolvedFunctionVariant, CallableParametersAcceptor { /** @@ -27,6 +27,7 @@ public function __construct( private array $impurePoints, private array $invalidateExpressions, private array $usedVariables, + private TrinaryLogic $acceptsNamedArguments, ) { } @@ -66,11 +67,6 @@ public function getReturnTypeWithUnresolvableTemplateTypes(): Type return $this->parametersAcceptor->getReturnTypeWithUnresolvableTemplateTypes(); } - public function getPhpDocReturnTypeWithUnresolvableTemplateTypes(): Type - { - return $this->parametersAcceptor->getPhpDocReturnTypeWithUnresolvableTemplateTypes(); - } - public function getReturnType(): Type { return $this->parametersAcceptor->getReturnType(); @@ -111,4 +107,9 @@ public function getUsedVariables(): array return $this->usedVariables; } + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->acceptsNamedArguments; + } + } diff --git a/src/Reflection/ResolvedFunctionVariantWithOriginal.php b/src/Reflection/ResolvedFunctionVariantWithOriginal.php index 6d72ef75bc..d7d2f09acc 100644 --- a/src/Reflection/ResolvedFunctionVariantWithOriginal.php +++ b/src/Reflection/ResolvedFunctionVariantWithOriginal.php @@ -2,11 +2,11 @@ namespace PHPStan\Reflection; -use PHPStan\DependencyInjection\BleedingEdgeToggle; -use PHPStan\Reflection\Php\DummyParameterWithPhpDocs; +use PHPStan\Reflection\Php\ExtendedDummyParameter; use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\ErrorType; 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; @@ -19,10 +19,10 @@ use function array_key_exists; use function array_map; -class ResolvedFunctionVariantWithOriginal implements ResolvedFunctionVariant +final class ResolvedFunctionVariantWithOriginal implements ResolvedFunctionVariant { - /** @var ParameterReflectionWithPhpDocs[]|null */ + /** @var list|null */ private ?array $parameters = null; private ?Type $returnTypeWithUnresolvableTemplateTypes = null; @@ -37,7 +37,7 @@ class ResolvedFunctionVariantWithOriginal implements ResolvedFunctionVariant * @param array $passedArgs */ public function __construct( - private ParametersAcceptorWithPhpDocs $parametersAcceptor, + private ExtendedParametersAcceptor $parametersAcceptor, private TemplateTypeMap $resolvedTemplateTypeMap, private TemplateTypeVarianceMap $callSiteVarianceMap, private array $passedArgs, @@ -71,7 +71,7 @@ public function getParameters(): array if ($parameters === null) { $parameters = array_map( - function (ParameterReflectionWithPhpDocs $param): ParameterReflectionWithPhpDocs { + function (ExtendedParameterReflection $param): ExtendedParameterReflection { $paramType = TypeUtils::resolveLateResolvableTypes( TemplateTypeHelper::resolveTemplateTypes( $this->resolveConditionalTypesForParameter($param->getType()), @@ -108,7 +108,7 @@ function (ParameterReflectionWithPhpDocs $param): ParameterReflectionWithPhpDocs ); } - return new DummyParameterWithPhpDocs( + return new ExtendedDummyParameter( $param->getName(), $paramType, $param->isOptional(), @@ -120,6 +120,7 @@ function (ParameterReflectionWithPhpDocs $param): ParameterReflectionWithPhpDocs $paramOutType, $param->isImmediatelyInvokedCallable(), $closureThisType, + $param->getAttributes(), ); }, $this->parametersAcceptor->getParameters(), @@ -245,7 +246,7 @@ private function resolveResolvableTemplateTypes(Type $type, TemplateTypeVariance }; return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($references, $objectCb): Type { - if (BleedingEdgeToggle::isBleedingEdge() && $type instanceof GenericObjectType) { + if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) { return TypeTraverser::map($type, $objectCb); } diff --git a/src/Reflection/ResolvedMethodReflection.php b/src/Reflection/ResolvedMethodReflection.php index 31433330b8..134e566eca 100644 --- a/src/Reflection/ResolvedMethodReflection.php +++ b/src/Reflection/ResolvedMethodReflection.php @@ -11,13 +11,13 @@ use PHPStan\Type\Type; use function is_bool; -class ResolvedMethodReflection implements ExtendedMethodReflection +final class ResolvedMethodReflection implements ExtendedMethodReflection { - /** @var ParametersAcceptorWithPhpDocs[]|null */ + /** @var list|null */ private ?array $variants = null; - /** @var ParametersAcceptorWithPhpDocs[]|null */ + /** @var list|null */ private ?array $namedArgumentVariants = null; private ?Assertions $asserts = null; @@ -52,6 +52,11 @@ public function getVariants(): array return $this->variants = $this->resolveVariants($this->reflection->getVariants()); } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + public function getNamedArgumentsVariants(): ?array { $variants = $this->namedArgumentVariants; @@ -68,8 +73,8 @@ public function getNamedArgumentsVariants(): ?array } /** - * @param ParametersAcceptorWithPhpDocs[] $variants - * @return ResolvedFunctionVariant[] + * @param ExtendedParametersAcceptor[] $variants + * @return list */ private function resolveVariants(array $variants): array { @@ -145,6 +150,16 @@ 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(); @@ -170,6 +185,11 @@ public function getAsserts(): Assertions )); } + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->reflection->acceptsNamedArguments(); + } + public function getSelfOutType(): ?Type { if ($this->selfOutType === false) { @@ -204,4 +224,9 @@ public function isAbstract(): TrinaryLogic return $abstract; } + public function getAttributes(): array + { + return $this->reflection->getAttributes(); + } + } diff --git a/src/Reflection/ResolvedPropertyReflection.php b/src/Reflection/ResolvedPropertyReflection.php index 7888b26abd..7646de8651 100644 --- a/src/Reflection/ResolvedPropertyReflection.php +++ b/src/Reflection/ResolvedPropertyReflection.php @@ -2,7 +2,6 @@ namespace PHPStan\Reflection; -use PHPStan\Reflection\Php\PhpPropertyReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; @@ -10,7 +9,7 @@ use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; -class ResolvedPropertyReflection implements WrapperPropertyReflection +final class ResolvedPropertyReflection implements WrapperPropertyReflection { private ?Type $readableType = null; @@ -18,30 +17,26 @@ class ResolvedPropertyReflection implements WrapperPropertyReflection private ?Type $writableType = null; public function __construct( - private PropertyReflection $reflection, + private ExtendedPropertyReflection $reflection, private TemplateTypeMap $templateTypeMap, private TemplateTypeVarianceMap $callSiteVarianceMap, ) { } - 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; @@ -144,4 +159,53 @@ 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(); + } + } diff --git a/src/Reflection/SignatureMap/FunctionSignature.php b/src/Reflection/SignatureMap/FunctionSignature.php index 8473684acc..f9107d4b23 100644 --- a/src/Reflection/SignatureMap/FunctionSignature.php +++ b/src/Reflection/SignatureMap/FunctionSignature.php @@ -4,11 +4,11 @@ use PHPStan\Type\Type; -class FunctionSignature +final class FunctionSignature { /** - * @param array $parameters + * @param list $parameters */ public function __construct( private array $parameters, @@ -20,7 +20,7 @@ public function __construct( } /** - * @return array + * @return list */ public function getParameters(): array { diff --git a/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php b/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php index 66ca12c487..3269191b79 100644 --- a/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php +++ b/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php @@ -4,6 +4,8 @@ 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; @@ -19,7 +21,8 @@ use function strtolower; use const CASE_LOWER; -class FunctionSignatureMapProvider implements SignatureMapProvider +#[AutowiredService(as: FunctionSignatureMapProvider::class)] +final class FunctionSignatureMapProvider implements SignatureMapProvider { /** @var array */ @@ -32,6 +35,7 @@ public function __construct( private SignatureMapParser $parser, private InitializerExprTypeResolver $initializerExprTypeResolver, private PhpVersion $phpVersion, + #[AutowiredParameter(ref: '%featureToggles.stricterFunctionMap%')] private bool $stricterFunctionMap, ) { diff --git a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index 7c850a582e..202b74ddfc 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -6,12 +6,16 @@ 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\Assertions; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; +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\NativeParameterWithPhpDocsReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeMap; @@ -22,13 +26,21 @@ use function array_map; use function strtolower; -class NativeFunctionReflectionProvider +#[AutowiredService] +final class NativeFunctionReflectionProvider { /** @var NativeFunctionReflection[] */ private array $functionMap = []; - public function __construct(private SignatureMapProvider $signatureMapProvider, private Reflector $reflector, private FileTypeMapper $fileTypeMapper, private StubPhpDocProvider $stubPhpDocProvider) + public function __construct( + private SignatureMapProvider $signatureMapProvider, + #[AutowiredParameter(ref: '@betterReflectionReflector')] + private Reflector $reflector, + private FileTypeMapper $fileTypeMapper, + private StubPhpDocProvider $stubPhpDocProvider, + private AttributeReflectionFactory $attributeReflectionFactory, + ) { } @@ -51,11 +63,16 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $asserts = Assertions::createEmpty(); $docComment = null; $returnsByReference = TrinaryLogic::createMaybe(); + $acceptsNamedArguments = true; + $fileName = null; + $attributes = []; try { $reflectionFunction = $this->reflector->reflectFunction($functionName); $reflectionFunctionAdapter = new ReflectionFunction($reflectionFunction); + $attributes = $reflectionFunctionAdapter->getAttributes(); $returnsByReference = TrinaryLogic::createFromBoolean($reflectionFunctionAdapter->returnsReference()); $realFunctionName = $reflectionFunction->getName(); + $isDeprecated = $reflectionFunction->isDeprecated(); if ($reflectionFunction->getFileName() !== null) { $fileName = $reflectionFunction->getFileName(); $docComment = $reflectionFunction->getDocComment(); @@ -65,7 +82,6 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef if ($throwsTag !== null) { $throwType = $throwsTag->getType(); } - $isDeprecated = $reflectionFunction->isDeprecated(); } } } catch (IdentifierNotFound | InvalidIdentifierName) { @@ -84,15 +100,16 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef } $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 FunctionVariantWithPhpDocs( + $variantsByType[$signatureType][] = new ExtendedFunctionVariant( TemplateTypeMap::createEmpty(), null, - array_map(static function (ParameterSignature $parameterSignature) use ($phpDoc): NativeParameterWithPhpDocsReflection { + array_map(static function (ParameterSignature $parameterSignature) use ($phpDoc): ExtendedNativeParameterReflection { $type = $parameterSignature->getType(); $phpDocType = null; @@ -110,7 +127,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef } } - return new NativeParameterWithPhpDocsReflection( + return new ExtendedNativeParameterReflection( $parameterSignature->getName(), $parameterSignature->isOptional(), TypehintHelper::decideType($type, $phpDocType), @@ -122,6 +139,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $phpDoc !== null ? NativeFunctionReflectionProvider::getParamOutTypeFromPhpDoc($parameterSignature->getName(), $phpDoc) : null, $immediatelyInvokedCallable, $closureThisType, + [], ); }, $functionSignature->getParameters()), $functionSignature->isVariadic(), @@ -148,6 +166,8 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $asserts, $docComment, $returnsByReference, + $acceptsNamedArguments, + $this->attributeReflectionFactory->fromNativeReflection($attributes, InitializerExprContext::fromFunction($realFunctionName, $fileName)), ); $this->functionMap[$lowerCasedFunctionName] = $functionReflection; diff --git a/src/Reflection/SignatureMap/ParameterSignature.php b/src/Reflection/SignatureMap/ParameterSignature.php index fc9316c300..7649825119 100644 --- a/src/Reflection/SignatureMap/ParameterSignature.php +++ b/src/Reflection/SignatureMap/ParameterSignature.php @@ -5,7 +5,7 @@ use PHPStan\Reflection\PassedByReference; use PHPStan\Type\Type; -class ParameterSignature +final class ParameterSignature { public function __construct( diff --git a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php index d114609b35..61b1553782 100644 --- a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php +++ b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php @@ -3,12 +3,14 @@ namespace PHPStan\Reflection\SignatureMap; use PhpParser\Node\AttributeGroup; +use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\Variable; 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; @@ -16,11 +18,13 @@ use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\PassedByReference; +use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\MixedType; use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; use ReflectionFunctionAbstract; use function array_key_exists; @@ -31,7 +35,8 @@ use function sprintf; use function strtolower; -class Php8SignatureMapProvider implements SignatureMapProvider +#[AutowiredService(as: Php8SignatureMapProvider::class)] +final class Php8SignatureMapProvider implements SignatureMapProvider { private const DIRECTORY = __DIR__ . '/../../../vendor/phpstan/php-8-stubs'; @@ -50,6 +55,7 @@ public function __construct( private FileTypeMapper $fileTypeMapper, private PhpVersion $phpVersion, private InitializerExprTypeResolver $initializerExprTypeResolver, + private ReflectionProviderProvider $reflectionProviderProvider, ) { $this->map = new Php8StubsMap($phpVersion->getVersionId()); @@ -58,9 +64,6 @@ public function __construct( public function hasMethodSignature(string $className, string $methodName): bool { $lowerClassName = strtolower($className); - if ($lowerClassName === 'backedenum') { - return false; - } if (!array_key_exists($lowerClassName, $this->map->classes)) { return $this->functionSignatureMapProvider->hasMethodSignature($className, $methodName); } @@ -268,8 +271,8 @@ private function getMergedSignatures(FunctionSignature $nativeSignature, array $ $functionParam->getNativeType(), $functionParam->passedByReference(), $functionParam->isVariadic(), - $functionParam->getDefaultValue(), - $functionParam->getOutType(), + $functionParam->getDefaultValue() ?? $nativeParam->getDefaultValue(), + $functionParam->getOutType() ?? $nativeParam->getOutType(), ); } @@ -392,6 +395,13 @@ private function getSignature( $phpDocReturnType = $phpDoc->getReturnTag()->getType(); } } + + $classReflection = null; + if ($className !== null) { + $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); + $classReflection = $reflectionProvider->getClass($className); + } + $parameters = []; $variadic = false; foreach ($function->getParams() as $param) { @@ -399,11 +409,24 @@ private function getSignature( if (!$name instanceof Variable || !is_string($name->name)) { throw new ShouldNotHappenException(); } - $parameterType = ParserNodeTypeToPHPStanType::resolve($param->type, null); + $parameterType = ParserNodeTypeToPHPStanType::resolve($param->type, $classReflection); + $phpDocParameterType = $phpDocParameterTypes[$name->name] ?? null; + + if ($param->default instanceof ConstFetch) { + $constName = (string) $param->default->name; + $loweredConstName = strtolower($constName); + if ($loweredConstName === 'null') { + $parameterType = TypeCombinator::addNull($parameterType); + if ($phpDocParameterType !== null) { + $phpDocParameterType = TypeCombinator::addNull($phpDocParameterType); + } + } + } + $parameters[] = new ParameterSignature( $name->name, $param->default !== null || $param->variadic, - TypehintHelper::decideType($parameterType, $phpDocParameterTypes[$name->name] ?? null), + TypehintHelper::decideType($parameterType, $phpDocParameterType), $parameterType, $param->byRef ? PassedByReference::createCreatesNewVariable() : PassedByReference::createNo(), $param->variadic, @@ -417,7 +440,7 @@ private function getSignature( $variadic = $variadic || $param->variadic; } - $returnType = ParserNodeTypeToPHPStanType::resolve($function->getReturnType(), null); + $returnType = ParserNodeTypeToPHPStanType::resolve($function->getReturnType(), $classReflection); return new FunctionSignature( $parameters, diff --git a/src/Reflection/SignatureMap/SignatureMapParser.php b/src/Reflection/SignatureMap/SignatureMapParser.php index b74f447ffa..fab2c15313 100644 --- a/src/Reflection/SignatureMap/SignatureMapParser.php +++ b/src/Reflection/SignatureMap/SignatureMapParser.php @@ -4,6 +4,7 @@ use Nette\Utils\Strings; use PHPStan\Analyser\NameScope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\PassedByReference; use PHPStan\ShouldNotHappenException; @@ -13,7 +14,8 @@ use function str_starts_with; use function substr; -class SignatureMapParser +#[AutowiredService] +final class SignatureMapParser { private TypeStringResolver $typeStringResolver; @@ -57,7 +59,7 @@ private function getTypeFromString(string $typeString, ?string $className): Type /** * @param array $parameterMap - * @return array + * @return list */ private function getParameters(array $parameterMap): array { diff --git a/src/Reflection/SignatureMap/SignatureMapProvider.php b/src/Reflection/SignatureMap/SignatureMapProvider.php index f7ec5ed5ce..3999d919b8 100644 --- a/src/Reflection/SignatureMap/SignatureMapProvider.php +++ b/src/Reflection/SignatureMap/SignatureMapProvider.php @@ -3,9 +3,11 @@ 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 { diff --git a/src/Reflection/SignatureMap/SignatureMapProviderFactory.php b/src/Reflection/SignatureMap/SignatureMapProviderFactory.php index 2e318eefeb..5e2c6a3fb3 100644 --- a/src/Reflection/SignatureMap/SignatureMapProviderFactory.php +++ b/src/Reflection/SignatureMap/SignatureMapProviderFactory.php @@ -2,9 +2,11 @@ namespace PHPStan\Reflection\SignatureMap; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; -class SignatureMapProviderFactory +#[AutowiredService] +final class SignatureMapProviderFactory { public function __construct( diff --git a/src/Reflection/TrivialParametersAcceptor.php b/src/Reflection/TrivialParametersAcceptor.php index 3f663f7909..ea6b278145 100644 --- a/src/Reflection/TrivialParametersAcceptor.php +++ b/src/Reflection/TrivialParametersAcceptor.php @@ -11,8 +11,10 @@ use PHPStan\Type\Type; use function sprintf; -/** @api */ -class TrivialParametersAcceptor implements ParametersAcceptorWithPhpDocs, CallableParametersAcceptor +/** + * @api + */ +final class TrivialParametersAcceptor implements ExtendedParametersAcceptor, CallableParametersAcceptor { /** @api */ @@ -91,4 +93,9 @@ public function getUsedVariables(): array return []; } + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + } diff --git a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php index a6f23841f3..2874a465fd 100644 --- a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php @@ -4,16 +4,18 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Dummy\ChangedTypeMethodReflection; +use PHPStan\Reflection\ExtendedFunctionVariant; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; -use PHPStan\Reflection\Php\DummyParameterWithPhpDocs; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; +use PHPStan\Reflection\Php\ExtendedDummyParameter; use PHPStan\Reflection\ResolvedMethodReflection; +use PHPStan\Type\ThisType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function array_map; -class CallbackUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection +final class CallbackUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { /** @var callable(Type): Type */ @@ -82,38 +84,60 @@ public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflectio private function transformMethodWithStaticType(ClassReflection $declaringClass, ExtendedMethodReflection $method): ExtendedMethodReflection { - $variantFn = fn (ParametersAcceptorWithPhpDocs $acceptor): ParametersAcceptorWithPhpDocs => new FunctionVariantWithPhpDocs( - $acceptor->getTemplateTypeMap(), - $acceptor->getResolvedTemplateTypeMap(), - array_map( - fn (ParameterReflectionWithPhpDocs $parameter): ParameterReflectionWithPhpDocs => new DummyParameterWithPhpDocs( - $parameter->getName(), - $this->transformStaticType($parameter->getType()), - $parameter->isOptional(), - $parameter->passedByReference(), - $parameter->isVariadic(), - $parameter->getDefaultValue(), - $parameter->getNativeType(), - $this->transformStaticType($parameter->getPhpDocType()), - $parameter->getOutType() !== null ? $this->transformStaticType($parameter->getOutType()) : null, - $parameter->isImmediatelyInvokedCallable(), - $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, + $selfOutType = $method->getSelfOutType() !== null ? $this->transformStaticType($method->getSelfOutType()) : null; + $variantFn = function (ExtendedParametersAcceptor $acceptor) use (&$selfOutType): ExtendedParametersAcceptor { + $originalReturnType = $acceptor->getReturnType(); + if ($originalReturnType instanceof ThisType && $selfOutType !== null) { + $returnType = TypeCombinator::intersect($selfOutType, $this->transformStaticType($originalReturnType)); + $selfOutType = $returnType; + } else { + $returnType = $this->transformStaticType($originalReturnType); + } + return new ExtendedFunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_map( + fn (ExtendedParameterReflection $parameter): ExtendedParameterReflection => new ExtendedDummyParameter( + $parameter->getName(), + $this->transformStaticType($parameter->getType()), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + $parameter->getNativeType(), + $this->transformStaticType($parameter->getPhpDocType()), + $parameter->getOutType() !== null ? $this->transformStaticType($parameter->getOutType()) : null, + $parameter->isImmediatelyInvokedCallable(), + $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, + $parameter->getAttributes(), + ), + $acceptor->getParameters(), ), - $acceptor->getParameters(), - ), - $acceptor->isVariadic(), - $this->transformStaticType($acceptor->getReturnType()), - $this->transformStaticType($acceptor->getPhpDocReturnType()), - $this->transformStaticType($acceptor->getNativeReturnType()), - $acceptor->getCallSiteVarianceMap(), - ); + $acceptor->isVariadic(), + $returnType, + $this->transformStaticType($acceptor->getPhpDocReturnType()), + $this->transformStaticType($acceptor->getNativeReturnType()), + $acceptor->getCallSiteVarianceMap(), + ); + }; $variants = array_map($variantFn, $method->getVariants()); $namedArgumentVariants = $method->getNamedArgumentsVariants(); $namedArgumentVariants = $namedArgumentVariants !== null ? array_map($variantFn, $namedArgumentVariants) : null; + $throwType = $method->getThrowType(); + $throwType = $throwType !== null + ? $this->transformStaticType($throwType) + : null; - return new ChangedTypeMethodReflection($declaringClass, $method, $variants, $namedArgumentVariants); + return new ChangedTypeMethodReflection( + $declaringClass, + $method, + $variants, + $namedArgumentVariants, + $selfOutType, + $throwType, + ); } private function transformStaticType(Type $type): Type diff --git a/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php index 5140d7296b..06069f8410 100644 --- a/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php @@ -4,17 +4,17 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Dummy\ChangedTypePropertyReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\ResolvedPropertyReflection; use PHPStan\Type\Type; -class CallbackUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection +final class CallbackUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { /** @var callable(Type): Type */ private $transformStaticTypeCallback; - private ?PropertyReflection $transformedProperty = null; + private ?ExtendedPropertyReflection $transformedProperty = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -22,7 +22,7 @@ class CallbackUnresolvedPropertyPrototypeReflection implements UnresolvedPropert * @param callable(Type): Type $transformStaticTypeCallback */ public function __construct( - private PropertyReflection $propertyReflection, + private ExtendedPropertyReflection $propertyReflection, private ClassReflection $resolvedDeclaringClass, private bool $resolveTemplateTypeMapToBounds, callable $transformStaticTypeCallback, @@ -45,12 +45,12 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototy ); } - public function getNakedProperty(): PropertyReflection + public function getNakedProperty(): ExtendedPropertyReflection { return $this->propertyReflection; } - public function getTransformedProperty(): PropertyReflection + public function getTransformedProperty(): ExtendedPropertyReflection { if ($this->transformedProperty !== null) { return $this->transformedProperty; @@ -75,12 +75,14 @@ public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflect ); } - private function transformPropertyWithStaticType(ClassReflection $declaringClass, PropertyReflection $property): PropertyReflection + private function transformPropertyWithStaticType(ClassReflection $declaringClass, ExtendedPropertyReflection $property): ExtendedPropertyReflection { $readableType = $this->transformStaticType($property->getReadableType()); $writableType = $this->transformStaticType($property->getWritableType()); + $phpDocType = $this->transformStaticType($property->getPhpDocType()); + $nativeType = $this->transformStaticType($property->getNativeType()); - return new ChangedTypePropertyReflection($declaringClass, $property, $readableType, $writableType); + return new ChangedTypePropertyReflection($declaringClass, $property, $readableType, $writableType, $phpDocType, $nativeType); } private function transformStaticType(Type $type): Type diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php index bf4421d1f7..44709d32ae 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php @@ -4,18 +4,21 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Dummy\ChangedTypeMethodReflection; +use PHPStan\Reflection\ExtendedFunctionVariant; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; -use PHPStan\Reflection\Php\DummyParameterWithPhpDocs; +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 ?ExtendedMethodReflection $transformedMethod = null; @@ -77,43 +80,74 @@ public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflectio private function transformMethodWithStaticType(ClassReflection $declaringClass, ExtendedMethodReflection $method): ExtendedMethodReflection { - $variantFn = fn (ParametersAcceptorWithPhpDocs $acceptor): ParametersAcceptorWithPhpDocs => new FunctionVariantWithPhpDocs( - $acceptor->getTemplateTypeMap(), - $acceptor->getResolvedTemplateTypeMap(), - array_map( - fn (ParameterReflectionWithPhpDocs $parameter): ParameterReflectionWithPhpDocs => new DummyParameterWithPhpDocs( - $parameter->getName(), - $this->transformStaticType($parameter->getType()), - $parameter->isOptional(), - $parameter->passedByReference(), - $parameter->isVariadic(), - $parameter->getDefaultValue(), - $parameter->getNativeType(), - $this->transformStaticType($parameter->getPhpDocType()), - $parameter->getOutType() !== null ? $this->transformStaticType($parameter->getOutType()) : null, - $parameter->isImmediatelyInvokedCallable(), - $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, + $selfOutType = $method->getSelfOutType() !== null ? $this->transformStaticType($method->getSelfOutType()) : null; + $variantFn = function (ExtendedParametersAcceptor $acceptor) use ($selfOutType): ExtendedParametersAcceptor { + $originalReturnType = $acceptor->getReturnType(); + if ($originalReturnType instanceof ThisType && $selfOutType !== null) { + $returnType = $selfOutType; + } else { + $returnType = $this->transformStaticType($originalReturnType); + } + return new ExtendedFunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_map( + fn (ExtendedParameterReflection $parameter): ExtendedParameterReflection => new ExtendedDummyParameter( + $parameter->getName(), + $this->transformStaticType($parameter->getType()), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + $parameter->getNativeType(), + $this->transformStaticType($parameter->getPhpDocType()), + $parameter->getOutType() !== null ? $this->transformStaticType($parameter->getOutType()) : null, + $parameter->isImmediatelyInvokedCallable(), + $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, + $parameter->getAttributes(), + ), + $acceptor->getParameters(), ), - $acceptor->getParameters(), - ), - $acceptor->isVariadic(), - $this->transformStaticType($acceptor->getReturnType()), - $this->transformStaticType($acceptor->getPhpDocReturnType()), - $this->transformStaticType($acceptor->getNativeReturnType()), - $acceptor->getCallSiteVarianceMap(), - ); + $acceptor->isVariadic(), + $returnType, + $this->transformStaticType($acceptor->getPhpDocReturnType()), + $this->transformStaticType($acceptor->getNativeReturnType()), + $acceptor->getCallSiteVarianceMap(), + ); + }; $variants = array_map($variantFn, $method->getVariants()); $namedArgumentsVariants = $method->getNamedArgumentsVariants(); $namedArgumentsVariants = $namedArgumentsVariants !== null ? array_map($variantFn, $namedArgumentsVariants) : null; + $throwType = $method->getThrowType(); + $throwType = $throwType !== null + ? $this->transformStaticType($throwType) + : null; - return new ChangedTypeMethodReflection($declaringClass, $method, $variants, $namedArgumentsVariants); + return new ChangedTypeMethodReflection( + $declaringClass, + $method, + $variants, + $namedArgumentsVariants, + $selfOutType, + $throwType, + ); } 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 e9a8b8a161..18beaf3f8e 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php @@ -4,21 +4,21 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Dummy\ChangedTypePropertyReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\ResolvedPropertyReflection; use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; -class CalledOnTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection +final class CalledOnTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { - private ?PropertyReflection $transformedProperty = null; + private ?ExtendedPropertyReflection $transformedProperty = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; public function __construct( - private PropertyReflection $propertyReflection, + private ExtendedPropertyReflection $propertyReflection, private ClassReflection $resolvedDeclaringClass, private bool $resolveTemplateTypeMapToBounds, private Type $fetchedOnType, @@ -40,12 +40,12 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototy ); } - public function getNakedProperty(): PropertyReflection + public function getNakedProperty(): ExtendedPropertyReflection { return $this->propertyReflection; } - public function getTransformedProperty(): PropertyReflection + public function getTransformedProperty(): ExtendedPropertyReflection { if ($this->transformedProperty !== null) { return $this->transformedProperty; @@ -70,12 +70,14 @@ public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflect ); } - private function transformPropertyWithStaticType(ClassReflection $declaringClass, PropertyReflection $property): PropertyReflection + private function transformPropertyWithStaticType(ClassReflection $declaringClass, ExtendedPropertyReflection $property): ExtendedPropertyReflection { $readableType = $this->transformStaticType($property->getReadableType()); $writableType = $this->transformStaticType($property->getWritableType()); + $phpDocType = $this->transformStaticType($property->getPhpDocType()); + $nativeType = $this->transformStaticType($property->getNativeType()); - return new ChangedTypePropertyReflection($declaringClass, $property, $readableType, $writableType); + return new ChangedTypePropertyReflection($declaringClass, $property, $readableType, $writableType, $phpDocType, $nativeType); } private function transformStaticType(Type $type): Type diff --git a/src/Reflection/Type/IntersectionTypeMethodReflection.php b/src/Reflection/Type/IntersectionTypeMethodReflection.php index fb9a8ecdd3..eafd314157 100644 --- a/src/Reflection/Type/IntersectionTypeMethodReflection.php +++ b/src/Reflection/Type/IntersectionTypeMethodReflection.php @@ -5,11 +5,12 @@ use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedFunctionVariant; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -18,7 +19,7 @@ use function implode; use function is_bool; -class IntersectionTypeMethodReflection implements ExtendedMethodReflection +final class IntersectionTypeMethodReflection implements ExtendedMethodReflection { /** @@ -82,7 +83,7 @@ public function getVariants(): array $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 (ParametersAcceptorWithPhpDocs $acceptor): ParametersAcceptorWithPhpDocs => new FunctionVariantWithPhpDocs( + return array_map(static fn (ExtendedParametersAcceptor $acceptor): ExtendedParametersAcceptor => new ExtendedFunctionVariant( $acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), $acceptor->getParameters(), @@ -94,6 +95,16 @@ public function getVariants(): array ), $this->methods[0]->getVariants()); } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + $variants = $this->getVariants(); + if (count($variants) !== 1) { + throw new ShouldNotHappenException(); + } + + return $variants[0]; + } + public function getNamedArgumentsVariants(): ?array { return null; @@ -141,6 +152,11 @@ public function isInternal(): TrinaryLogic 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 { $types = []; @@ -187,6 +203,11 @@ public function getAsserts(): Assertions return $assertions; } + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->acceptsNamedArguments()); + } + public function getSelfOutType(): ?Type { return null; @@ -202,4 +223,9 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::lazyMaxMin($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => is_bool($method->isAbstract()) ? TrinaryLogic::createFromBoolean($method->isAbstract()) : $method->isAbstract()); } + public function getAttributes(): array + { + return $this->methods[0]->getAttributes(); + } + } diff --git a/src/Reflection/Type/IntersectionTypePropertyReflection.php b/src/Reflection/Type/IntersectionTypePropertyReflection.php index e93cc59e67..1e3e4d3179 100644 --- a/src/Reflection/Type/IntersectionTypePropertyReflection.php +++ b/src/Reflection/Type/IntersectionTypePropertyReflection.php @@ -3,7 +3,9 @@ namespace PHPStan\Reflection\Type; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -11,16 +13,21 @@ use function count; use function implode; -class IntersectionTypePropertyReflection implements PropertyReflection +final class IntersectionTypePropertyReflection implements ExtendedPropertyReflection { /** - * @param PropertyReflection[] $properties + * @param ExtendedPropertyReflection[] $properties */ public function __construct(private array $properties) { } + public function getName(): string + { + return $this->properties[0]->getName(); + } + public function getDeclaringClass(): ClassReflection { return $this->properties[0]->getDeclaringClass(); @@ -28,22 +35,22 @@ public function getDeclaringClass(): ClassReflection public function isStatic(): bool { - return $this->computeResult(static fn (PropertyReflection $property) => $property->isStatic()); + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isStatic()); } public function isPrivate(): bool { - return $this->computeResult(static fn (PropertyReflection $property) => $property->isPrivate()); + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPrivate()); } public function isPublic(): bool { - return $this->computeResult(static fn (PropertyReflection $property) => $property->isPublic()); + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPublic()); } public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::lazyMaxMin($this->properties, static fn (PropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isDeprecated()); + return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isDeprecated()); } public function getDeprecatedDescription(): ?string @@ -70,7 +77,7 @@ public function getDeprecatedDescription(): ?string public function isInternal(): TrinaryLogic { - return TrinaryLogic::lazyMaxMin($this->properties, static fn (PropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isInternal()); + return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isInternal()); } public function getDocComment(): ?string @@ -78,33 +85,53 @@ public function getDocComment(): ?string return null; } + public function hasPhpDocType(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->hasPhpDocType()); + } + + public function getPhpDocType(): Type + { + return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getPhpDocType(), $this->properties)); + } + + public function hasNativeType(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->hasNativeType()); + } + + public function getNativeType(): Type + { + return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getNativeType(), $this->properties)); + } + public function getReadableType(): Type { - return TypeCombinator::intersect(...array_map(static fn (PropertyReflection $property): Type => $property->getReadableType(), $this->properties)); + return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getReadableType(), $this->properties)); } public function getWritableType(): Type { - return TypeCombinator::intersect(...array_map(static fn (PropertyReflection $property): Type => $property->getWritableType(), $this->properties)); + return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getWritableType(), $this->properties)); } public function canChangeTypeAfterAssignment(): bool { - return $this->computeResult(static fn (PropertyReflection $property) => $property->canChangeTypeAfterAssignment()); + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->canChangeTypeAfterAssignment()); } public function isReadable(): bool { - return $this->computeResult(static fn (PropertyReflection $property) => $property->isReadable()); + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isReadable()); } public function isWritable(): bool { - return $this->computeResult(static fn (PropertyReflection $property) => $property->isWritable()); + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isWritable()); } /** - * @param callable(PropertyReflection): bool $cb + * @param callable(ExtendedPropertyReflection): bool $cb */ private function computeResult(callable $cb): bool { @@ -116,4 +143,66 @@ private function computeResult(callable $cb): bool return $result; } + 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->hasHook($hookType)) { + continue; + } + + $hooks[] = $property->getHook($hookType); + } + + if (count($hooks) === 0) { + throw new ShouldNotHappenException(); + } + + if (count($hooks) === 1) { + return $hooks[0]; + } + + return new IntersectionTypeMethodReflection($hooks[0]->getName(), $hooks); + } + + public function isProtectedSet(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isProtectedSet()); + } + + public function isPrivateSet(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPrivateSet()); + } + + public function getAttributes(): array + { + return $this->properties[0]->getAttributes(); + } + } diff --git a/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php index 5cda2321f1..fe3e09bd4e 100644 --- a/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php @@ -7,7 +7,7 @@ use PHPStan\Type\Type; use function array_map; -class IntersectionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection +final class IntersectionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { private ?ExtendedMethodReflection $transformedMethod = null; diff --git a/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php index fdfea4ebf9..51e6ccaf46 100644 --- a/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php @@ -2,14 +2,15 @@ namespace PHPStan\Reflection\Type; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\PropertyReflection; use PHPStan\Type\Type; use function array_map; -class IntersectionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection +final class IntersectionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { - private ?PropertyReflection $transformedProperty = null; + private ?ExtendedPropertyReflection $transformedProperty = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -32,12 +33,12 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototy return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->propertyName, array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->propertyPrototypes)); } - public function getNakedProperty(): PropertyReflection + public function getNakedProperty(): ExtendedPropertyReflection { return $this->getTransformedProperty(); } - public function getTransformedProperty(): PropertyReflection + public function getTransformedProperty(): ExtendedPropertyReflection { if ($this->transformedProperty !== null) { return $this->transformedProperty; diff --git a/src/Reflection/Type/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index 65ca5bd674..b330b6fdad 100644 --- a/src/Reflection/Type/UnionTypeMethodReflection.php +++ b/src/Reflection/Type/UnionTypeMethodReflection.php @@ -6,6 +6,7 @@ use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\TrinaryLogic; @@ -17,7 +18,7 @@ use function implode; use function is_bool; -class UnionTypeMethodReflection implements ExtendedMethodReflection +final class UnionTypeMethodReflection implements ExtendedMethodReflection { /** @@ -82,6 +83,11 @@ public function getVariants(): array return [ParametersAcceptorSelector::combineAcceptors($variants)]; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + public function getNamedArgumentsVariants(): ?array { return null; @@ -126,7 +132,12 @@ public function isFinalByKeyword(): TrinaryLogic public function isInternal(): TrinaryLogic { - return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->isInternal()); + 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 @@ -169,6 +180,11 @@ 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; @@ -184,4 +200,9 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => is_bool($method->isAbstract()) ? TrinaryLogic::createFromBoolean($method->isAbstract()) : $method->isAbstract()); } + public function getAttributes(): array + { + return $this->methods[0]->getAttributes(); + } + } diff --git a/src/Reflection/Type/UnionTypePropertyReflection.php b/src/Reflection/Type/UnionTypePropertyReflection.php index d2587839c3..1862d3059f 100644 --- a/src/Reflection/Type/UnionTypePropertyReflection.php +++ b/src/Reflection/Type/UnionTypePropertyReflection.php @@ -3,7 +3,9 @@ namespace PHPStan\Reflection\Type; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -11,16 +13,21 @@ use function count; use function implode; -class UnionTypePropertyReflection implements PropertyReflection +final class UnionTypePropertyReflection implements ExtendedPropertyReflection { /** - * @param PropertyReflection[] $properties + * @param ExtendedPropertyReflection[] $properties */ public function __construct(private array $properties) { } + public function getName(): string + { + return $this->properties[0]->getName(); + } + public function getDeclaringClass(): ClassReflection { return $this->properties[0]->getDeclaringClass(); @@ -28,22 +35,22 @@ public function getDeclaringClass(): ClassReflection public function isStatic(): bool { - return $this->computeResult(static fn (PropertyReflection $property) => $property->isStatic()); + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isStatic()); } public function isPrivate(): bool { - return $this->computeResult(static fn (PropertyReflection $property) => $property->isPrivate()); + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPrivate()); } public function isPublic(): bool { - return $this->computeResult(static fn (PropertyReflection $property) => $property->isPublic()); + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPublic()); } public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (PropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isDeprecated()); + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isDeprecated()); } public function getDeprecatedDescription(): ?string @@ -70,7 +77,7 @@ public function getDeprecatedDescription(): ?string public function isInternal(): TrinaryLogic { - return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (PropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isInternal()); + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isInternal()); } public function getDocComment(): ?string @@ -78,33 +85,53 @@ public function getDocComment(): ?string return null; } + public function hasPhpDocType(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->hasPhpDocType()); + } + + public function getPhpDocType(): Type + { + return TypeCombinator::union(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getPhpDocType(), $this->properties)); + } + + public function hasNativeType(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->hasNativeType()); + } + + public function getNativeType(): Type + { + return TypeCombinator::union(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getNativeType(), $this->properties)); + } + public function getReadableType(): Type { - return TypeCombinator::union(...array_map(static fn (PropertyReflection $property): Type => $property->getReadableType(), $this->properties)); + return TypeCombinator::union(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getReadableType(), $this->properties)); } public function getWritableType(): Type { - return TypeCombinator::union(...array_map(static fn (PropertyReflection $property): Type => $property->getWritableType(), $this->properties)); + return TypeCombinator::union(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getWritableType(), $this->properties)); } public function canChangeTypeAfterAssignment(): bool { - return $this->computeResult(static fn (PropertyReflection $property) => $property->canChangeTypeAfterAssignment()); + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->canChangeTypeAfterAssignment()); } public function isReadable(): bool { - return $this->computeResult(static fn (PropertyReflection $property) => $property->isReadable()); + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isReadable()); } public function isWritable(): bool { - return $this->computeResult(static fn (PropertyReflection $property) => $property->isWritable()); + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isWritable()); } /** - * @param callable(PropertyReflection): bool $cb + * @param callable(ExtendedPropertyReflection): bool $cb */ private function computeResult(callable $cb): bool { @@ -116,4 +143,66 @@ private function computeResult(callable $cb): bool return $result; } + 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->hasHook($hookType)) { + continue; + } + + $hooks[] = $property->getHook($hookType); + } + + if (count($hooks) === 0) { + throw new ShouldNotHappenException(); + } + + if (count($hooks) === 1) { + return $hooks[0]; + } + + return new UnionTypeMethodReflection($hooks[0]->getName(), $hooks); + } + + public function isProtectedSet(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isProtectedSet()); + } + + public function isPrivateSet(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPrivateSet()); + } + + public function getAttributes(): array + { + return $this->properties[0]->getAttributes(); + } + } diff --git a/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php index 9f0e62bc0c..627a1e5b80 100644 --- a/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php @@ -7,7 +7,7 @@ use PHPStan\Type\Type; use function array_map; -class UnionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection +final class UnionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { private ?ExtendedMethodReflection $transformedMethod = null; diff --git a/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php index fe47fa1452..b28625e3c3 100644 --- a/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php @@ -2,14 +2,15 @@ namespace PHPStan\Reflection\Type; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\PropertyReflection; use PHPStan\Type\Type; use function array_map; -class UnionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection +final class UnionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { - private ?PropertyReflection $transformedProperty = null; + private ?ExtendedPropertyReflection $transformedProperty = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -31,12 +32,12 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototy return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->propertyName, array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->propertyPrototypes)); } - public function getNakedProperty(): PropertyReflection + public function getNakedProperty(): ExtendedPropertyReflection { return $this->getTransformedProperty(); } - public function getTransformedProperty(): PropertyReflection + public function getTransformedProperty(): ExtendedPropertyReflection { if ($this->transformedProperty !== null) { return $this->transformedProperty; diff --git a/src/Reflection/Type/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 index c565601261..5a9ea238cf 100644 --- a/src/Reflection/WrappedExtendedMethodReflection.php +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -2,14 +2,14 @@ namespace PHPStan\Reflection; -use PHPStan\Reflection\Php\DummyParameterWithPhpDocs; +use PHPStan\Reflection\Php\ExtendedDummyParameter; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use function array_map; -class WrappedExtendedMethodReflection implements ExtendedMethodReflection +final class WrappedExtendedMethodReflection implements ExtendedMethodReflection { public function __construct(private MethodReflection $method) @@ -55,15 +55,15 @@ public function getVariants(): array { $variants = []; foreach ($this->method->getVariants() as $variant) { - if ($variant instanceof ParametersAcceptorWithPhpDocs) { + if ($variant instanceof ExtendedParametersAcceptor) { $variants[] = $variant; continue; } - $variants[] = new FunctionVariantWithPhpDocs( + $variants[] = new ExtendedFunctionVariant( $variant->getTemplateTypeMap(), $variant->getResolvedTemplateTypeMap(), - array_map(static fn (ParameterReflection $parameter): ParameterReflectionWithPhpDocs => $parameter instanceof ParameterReflectionWithPhpDocs ? $parameter : new DummyParameterWithPhpDocs( + array_map(static fn (ParameterReflection $parameter): ExtendedParameterReflection => $parameter instanceof ExtendedParameterReflection ? $parameter : new ExtendedDummyParameter( $parameter->getName(), $parameter->getType(), $parameter->isOptional(), @@ -75,6 +75,7 @@ public function getVariants(): array null, TrinaryLogic::createMaybe(), null, + [], ), $variant->getParameters()), $variant->isVariadic(), $variant->getReturnType(), @@ -87,6 +88,11 @@ public function getVariants(): array return $variants; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + public function getNamedArgumentsVariants(): ?array { return null; @@ -117,6 +123,11 @@ public function isInternal(): TrinaryLogic return $this->method->isInternal(); } + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function getThrowType(): ?Type { return $this->method->getThrowType(); @@ -137,6 +148,11 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->getDeclaringClass()->acceptsNamedArguments()); + } + public function getSelfOutType(): ?Type { return null; @@ -152,4 +168,9 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getAttributes(): array + { + return []; + } + } diff --git a/src/Reflection/WrappedExtendedPropertyReflection.php b/src/Reflection/WrappedExtendedPropertyReflection.php new file mode 100644 index 0000000000..ffb2a34363 --- /dev/null +++ b/src/Reflection/WrappedExtendedPropertyReflection.php @@ -0,0 +1,152 @@ +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 []; + } + +} 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 index 528d2daf8b..785c29d64f 100644 --- a/src/Rules/Api/ApiClassConstFetchRule.php +++ b/src/Rules/Api/ApiClassConstFetchRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,7 +15,8 @@ /** * @implements Rule */ -class ApiClassConstFetchRule implements Rule +#[RegisteredRule(level: 0)] +final class ApiClassConstFetchRule implements Rule { public function __construct( diff --git a/src/Rules/Api/ApiClassExtendsRule.php b/src/Rules/Api/ApiClassExtendsRule.php index 72aad19740..4a2e7c0942 100644 --- a/src/Rules/Api/ApiClassExtendsRule.php +++ b/src/Rules/Api/ApiClassExtendsRule.php @@ -6,6 +6,7 @@ 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; @@ -15,7 +16,8 @@ /** * @implements Rule */ -class ApiClassExtendsRule implements Rule +#[RegisteredRule(level: 0)] +final class ApiClassExtendsRule implements Rule { public function __construct( diff --git a/src/Rules/Api/ApiClassImplementsRule.php b/src/Rules/Api/ApiClassImplementsRule.php index 521279beca..c2bdf22c3e 100644 --- a/src/Rules/Api/ApiClassImplementsRule.php +++ b/src/Rules/Api/ApiClassImplementsRule.php @@ -5,6 +5,7 @@ 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; @@ -17,7 +18,8 @@ /** * @implements Rule */ -class ApiClassImplementsRule implements Rule +#[RegisteredRule(level: 0)] +final class ApiClassImplementsRule implements Rule { public function __construct( diff --git a/src/Rules/Api/ApiInstanceofRule.php b/src/Rules/Api/ApiInstanceofRule.php index 5f2c4d531a..13acf1d05c 100644 --- a/src/Rules/Api/ApiInstanceofRule.php +++ b/src/Rules/Api/ApiInstanceofRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\IdentifierRuleError; @@ -19,7 +20,8 @@ /** * @implements Rule */ -class ApiInstanceofRule implements Rule +#[RegisteredRule(level: 0)] +final class ApiInstanceofRule implements Rule { public function __construct( @@ -81,7 +83,10 @@ public function processNode(Node $node, Scope $scope): array */ private function processCoveredClass(Node\Expr\Instanceof_ $node, Scope $scope, ClassReflection $classReflection): array { - if ($classReflection->getName() === Type::class || $classReflection->isSubclassOf(Type::class)) { + if ($classReflection->is(Type::class)) { + return []; + } + if ($classReflection->isInterface()) { return []; } @@ -90,7 +95,7 @@ private function processCoveredClass(Node\Expr\Instanceof_ $node, Scope $scope, return []; } - $classType = new ObjectType($classReflection->getName(), null, $classReflection); + $classType = new ObjectType($classReflection->getName(), classReflection: $classReflection); $exprType = $scope->getType($node->expr); if ($exprType instanceof UnionType) { diff --git a/src/Rules/Api/ApiInstanceofTypeRule.php b/src/Rules/Api/ApiInstanceofTypeRule.php index fd4a3c6e20..c88c5114bf 100644 --- a/src/Rules/Api/ApiInstanceofTypeRule.php +++ b/src/Rules/Api/ApiInstanceofTypeRule.php @@ -5,16 +5,19 @@ use PhpParser\Node; use PhpParser\Node\Expr\Instanceof_; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Parser\TypeTraverserInstanceofVisitor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; 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\HasMethodType; use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasPropertyType; @@ -28,7 +31,6 @@ use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantScalarType; -use PHPStan\Type\ConstantType; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\FloatType; use PHPStan\Type\Generic\GenericClassStringType; @@ -50,7 +52,8 @@ /** * @implements Rule */ -class ApiInstanceofTypeRule implements Rule +#[RegisteredRule(level: 0)] +final class ApiInstanceofTypeRule implements Rule { private const MAP = [ @@ -74,7 +77,6 @@ class ApiInstanceofTypeRule implements Rule GenericClassStringType::class => 'Type::isClassStringType() and Type::getClassStringObjectType()', GenericObjectType::class => null, IntersectionType::class => null, - ConstantType::class => 'Type::isConstantValue() or Type::generalize()', ConstantScalarType::class => 'Type::isConstantScalarValue() or Type::getConstantScalarTypes() or Type::getConstantScalarValues()', ObjectShapeType::class => 'Type::isObject() and Type::hasProperty()', @@ -84,6 +86,8 @@ class ApiInstanceofTypeRule implements Rule 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()', @@ -94,8 +98,6 @@ class ApiInstanceofTypeRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, - private bool $enabled, - private bool $deprecationRulesInstalled, ) { } @@ -107,10 +109,6 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$this->enabled && !$this->deprecationRulesInstalled) { - return []; - } - if (!$node->class instanceof Node\Name) { return []; } @@ -132,7 +130,7 @@ public function processNode(Node $node, Scope $scope): array if ($this->reflectionProvider->hasClass($className)) { $classReflection = $this->reflectionProvider->getClass($className); - if ($classReflection->isSubclassOf(AccessoryType::class)) { + if ($classReflection->is(AccessoryType::class)) { if ($className === $classReflection->getName()) { return []; } diff --git a/src/Rules/Api/ApiInstantiationRule.php b/src/Rules/Api/ApiInstantiationRule.php index bea7e29b68..2939c8e6be 100644 --- a/src/Rules/Api/ApiInstantiationRule.php +++ b/src/Rules/Api/ApiInstantiationRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -13,7 +14,8 @@ /** * @implements Rule */ -class ApiInstantiationRule implements Rule +#[RegisteredRule(level: 0)] +final class ApiInstantiationRule implements Rule { public function __construct( diff --git a/src/Rules/Api/ApiInterfaceExtendsRule.php b/src/Rules/Api/ApiInterfaceExtendsRule.php index fc5d8ca505..6ea4db2d63 100644 --- a/src/Rules/Api/ApiInterfaceExtendsRule.php +++ b/src/Rules/Api/ApiInterfaceExtendsRule.php @@ -5,6 +5,7 @@ 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; @@ -17,7 +18,8 @@ /** * @implements Rule */ -class ApiInterfaceExtendsRule implements Rule +#[RegisteredRule(level: 0)] +final class ApiInterfaceExtendsRule implements Rule { public function __construct( diff --git a/src/Rules/Api/ApiMethodCallRule.php b/src/Rules/Api/ApiMethodCallRule.php index 52472fb47e..87b9bc5f4e 100644 --- a/src/Rules/Api/ApiMethodCallRule.php +++ b/src/Rules/Api/ApiMethodCallRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,7 +15,8 @@ /** * @implements Rule */ -class ApiMethodCallRule implements Rule +#[RegisteredRule(level: 0)] +final class ApiMethodCallRule implements Rule { public function __construct(private ApiRuleHelper $apiRuleHelper) diff --git a/src/Rules/Api/ApiRuleHelper.php b/src/Rules/Api/ApiRuleHelper.php index 021353bbd0..01801a49aa 100644 --- a/src/Rules/Api/ApiRuleHelper.php +++ b/src/Rules/Api/ApiRuleHelper.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Api; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\File\ParentDirectoryRelativePathHelper; use function dirname; use function pathinfo; @@ -11,7 +12,8 @@ use function strtolower; use const PATHINFO_BASENAME; -class ApiRuleHelper +#[AutowiredService] +final class ApiRuleHelper { public function isPhpStanCode(Scope $scope, string $namespace, ?string $declaringFile): bool diff --git a/src/Rules/Api/ApiStaticCallRule.php b/src/Rules/Api/ApiStaticCallRule.php index 6d48c393c0..5441c16572 100644 --- a/src/Rules/Api/ApiStaticCallRule.php +++ b/src/Rules/Api/ApiStaticCallRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; @@ -15,7 +16,8 @@ /** * @implements Rule */ -class ApiStaticCallRule implements Rule +#[RegisteredRule(level: 0)] +final class ApiStaticCallRule implements Rule { public function __construct( diff --git a/src/Rules/Api/ApiTraitUseRule.php b/src/Rules/Api/ApiTraitUseRule.php index 33da77eb36..796d745bab 100644 --- a/src/Rules/Api/ApiTraitUseRule.php +++ b/src/Rules/Api/ApiTraitUseRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -12,7 +13,8 @@ /** * @implements Rule */ -class ApiTraitUseRule implements Rule +#[RegisteredRule(level: 0)] +final class ApiTraitUseRule implements Rule { public function __construct( diff --git a/src/Rules/Api/BcUncoveredInterface.php b/src/Rules/Api/BcUncoveredInterface.php index 9a78dc5cc7..3bf9c0c61d 100644 --- a/src/Rules/Api/BcUncoveredInterface.php +++ b/src/Rules/Api/BcUncoveredInterface.php @@ -3,11 +3,23 @@ namespace PHPStan\Rules\Api; use PHPStan\Analyser\Scope; +use PHPStan\Command\Output; +use PHPStan\Command\OutputStyle; +use PHPStan\DependencyInjection\Container; +use PHPStan\Node\ReturnStatementsNode; +use PHPStan\Node\VirtualNode; +use PHPStan\PhpDoc\Tag\TypedTag; use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\ClassConstantReflection; +use PHPStan\Reflection\ClassMemberAccessAnswerer; +use PHPStan\Reflection\ClassMemberReflection; +use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\Reflection\NamespaceAnswerer; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\FileRuleError; use PHPStan\Rules\IdentifierRuleError; @@ -16,19 +28,28 @@ use PHPStan\Rules\NonIgnorableRuleError; use PHPStan\Rules\RuleError; use PHPStan\Rules\TipRuleError; +use PHPStan\Type\CompoundType; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Type; +use PHPStan\Type\TypeWithClassName; final class BcUncoveredInterface { public const CLASSES = [ Type::class, + CompoundType::class, + TemplateType::class, + TypedTag::class, + TypeWithClassName::class, + VirtualNode::class, ReflectionProvider::class, Scope::class, FunctionReflection::class, ExtendedMethodReflection::class, - ParametersAcceptorWithPhpDocs::class, - ParameterReflectionWithPhpDocs::class, + ExtendedPropertyReflection::class, + ExtendedParametersAcceptor::class, + ExtendedParameterReflection::class, CallableParametersAcceptor::class, FileRuleError::class, IdentifierRuleError::class, @@ -37,6 +58,15 @@ final class BcUncoveredInterface NonIgnorableRuleError::class, RuleError::class, TipRuleError::class, + Output::class, + ClassMemberReflection::class, + ConstantReflection::class, + ClassConstantReflection::class, + ClassMemberAccessAnswerer::class, + NamespaceAnswerer::class, + Container::class, + OutputStyle::class, + ReturnStatementsNode::class, ]; } diff --git a/src/Rules/Api/GetTemplateTypeRule.php b/src/Rules/Api/GetTemplateTypeRule.php index 05c48497d0..ee55ed567c 100644 --- a/src/Rules/Api/GetTemplateTypeRule.php +++ b/src/Rules/Api/GetTemplateTypeRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -15,7 +16,8 @@ /** * @implements Rule */ -class GetTemplateTypeRule implements Rule +#[RegisteredRule(level: 0)] +final class GetTemplateTypeRule implements Rule { public function __construct(private ReflectionProvider $reflectionProvider) diff --git a/src/Rules/Api/NodeConnectingVisitorAttributesRule.php b/src/Rules/Api/NodeConnectingVisitorAttributesRule.php index 0907413ca7..135a97465f 100644 --- a/src/Rules/Api/NodeConnectingVisitorAttributesRule.php +++ b/src/Rules/Api/NodeConnectingVisitorAttributesRule.php @@ -4,16 +4,12 @@ use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; -use PhpParser\NodeVisitor\NodeConnectingVisitor; use PHPStan\Analyser\Scope; -use PHPStan\DependencyInjection\Container; -use PHPStan\Parser\RichParser; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ObjectType; use function array_keys; -use function get_class; use function in_array; use function sprintf; use function str_starts_with; @@ -21,13 +17,10 @@ /** * @implements Rule */ -class NodeConnectingVisitorAttributesRule implements Rule +#[RegisteredRule(level: 0)] +final class NodeConnectingVisitorAttributesRule implements Rule { - public function __construct(private Container $container) - { - } - public function getNodeType(): string { return MethodCall::class; @@ -49,51 +42,40 @@ public function processNode(Node $node, Scope $scope): array if (!isset($args[0])) { return []; } + + $messages = []; $argType = $scope->getType($args[0]->value); - if (!$argType instanceof ConstantStringType) { - return []; - } - if (!in_array($argType->getValue(), ['parent', 'previous', 'next'], true)) { - return []; - } - if (!$scope->isInClass()) { - return []; - } + foreach ($argType->getConstantStrings() as $constantString) { + $argValue = $constantString->getValue(); + if (!in_array($argValue, ['parent', 'previous', 'next'], true)) { + continue; + } - $classReflection = $scope->getClassReflection(); - $hasPhpStanInterface = false; - foreach (array_keys($classReflection->getInterfaces()) as $interfaceName) { - if (!str_starts_with($interfaceName, 'PHPStan\\')) { + if (!$scope->isInClass()) { continue; } - $hasPhpStanInterface = true; - } + $classReflection = $scope->getClassReflection(); + $hasPhpStanInterface = false; + foreach (array_keys($classReflection->getInterfaces()) as $interfaceName) { + if (!str_starts_with($interfaceName, 'PHPStan\\')) { + continue; + } - if (!$hasPhpStanInterface) { - return []; - } + $hasPhpStanInterface = true; + } - $isVisitorRegistered = false; - foreach ($this->container->getServicesByTag(RichParser::VISITOR_SERVICE_TAG) as $service) { - if (get_class($service) !== NodeConnectingVisitor::class) { + if (!$hasPhpStanInterface) { continue; } - $isVisitorRegistered = true; - break; - } - - if ($isVisitorRegistered) { - return []; - } - - return [ - RuleErrorBuilder::message(sprintf('Node attribute \'%s\' is no longer available.', $argType->getValue())) + $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(), - ]; + ->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 cc449b8f30..bdfc145f9a 100644 --- a/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php +++ b/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php @@ -6,6 +6,7 @@ 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; @@ -18,7 +19,8 @@ /** * @implements Rule */ -class PhpStanNamespaceIn3rdPartyPackageRule implements Rule +#[RegisteredRule(level: 0)] +final class PhpStanNamespaceIn3rdPartyPackageRule implements Rule { public function __construct(private ApiRuleHelper $apiRuleHelper) diff --git a/src/Rules/Api/RuntimeReflectionFunctionRule.php b/src/Rules/Api/RuntimeReflectionFunctionRule.php index 8b8fbf6722..b46f7c7b36 100644 --- a/src/Rules/Api/RuntimeReflectionFunctionRule.php +++ b/src/Rules/Api/RuntimeReflectionFunctionRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -15,7 +16,8 @@ /** * @implements Rule */ -class RuntimeReflectionFunctionRule implements Rule +#[RegisteredRule(level: 0)] +final class RuntimeReflectionFunctionRule implements Rule { public function __construct(private ReflectionProvider $reflectionProvider) diff --git a/src/Rules/Api/RuntimeReflectionInstantiationRule.php b/src/Rules/Api/RuntimeReflectionInstantiationRule.php index 3e5dc7524c..ab3ae254fc 100644 --- a/src/Rules/Api/RuntimeReflectionInstantiationRule.php +++ b/src/Rules/Api/RuntimeReflectionInstantiationRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -25,7 +26,8 @@ /** * @implements Rule */ -class RuntimeReflectionInstantiationRule implements Rule +#[RegisteredRule(level: 0)] +final class RuntimeReflectionInstantiationRule implements Rule { public function __construct(private ReflectionProvider $reflectionProvider) diff --git a/src/Rules/Arrays/AllowedArrayKeysTypes.php b/src/Rules/Arrays/AllowedArrayKeysTypes.php index 6a2cc62542..2178b579fb 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,54 @@ public static function getType(): Type ]); } + public static function narrowOffsetKeyType(Type $varType, Type $keyType): ?Type + { + if (!$varType->isArray()->yes() || $varType->isIterableAtLeastOnce()->no()) { + return null; + } + + $varIterableKeyType = $varType->getIterableKeyType(); + + if ($varIterableKeyType->isConstantScalarValue()->yes()) { + $narrowedKey = TypeCombinator::union( + $varIterableKeyType, + TypeCombinator::remove($varIterableKeyType->toString(), new ConstantStringType('')), + ); + + if (!$varType->hasOffsetValueType(new ConstantIntegerType(0))->no()) { + $narrowedKey = TypeCombinator::union( + $narrowedKey, + new ConstantBooleanType(false), + ); + } + + if (!$varType->hasOffsetValueType(new ConstantIntegerType(1))->no()) { + $narrowedKey = TypeCombinator::union( + $narrowedKey, + new ConstantBooleanType(true), + ); + } + + if (!$varType->hasOffsetValueType(new ConstantStringType(''))->no()) { + $narrowedKey = TypeCombinator::addNull($narrowedKey); + } + + if (!$varIterableKeyType->isNumericString()->no() || !$varIterableKeyType->isInteger()->no()) { + $narrowedKey = TypeCombinator::union($narrowedKey, new FloatType()); + } + + return $narrowedKey; + } elseif ($varIterableKeyType->isInteger()->yes() && $keyType->isString()->yes()) { + return TypeCombinator::intersect($varIterableKeyType->toString(), $keyType); + } + + return new MixedType( + 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 96409f9aa1..0000000000 --- a/src/Rules/Arrays/AppendedArrayItemTypeRule.php +++ /dev/null @@ -1,93 +0,0 @@ - - */ -class AppendedArrayItemTypeRule implements Rule -{ - - public function __construct( - private PropertyReflectionFinder $propertyReflectionFinder, - private RuleLevelHelper $ruleLevelHelper, - ) - { - } - - public function getNodeType(): string - { - return Node\Expr::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if ( - !$node instanceof Assign - && !$node instanceof AssignOp - && !$node instanceof AssignRef - ) { - return []; - } - - if (!($node->var instanceof ArrayDimFetch)) { - return []; - } - - if ( - !$node->var->var instanceof Node\Expr\PropertyFetch - && !$node->var->var instanceof Node\Expr\StaticPropertyFetch - ) { - return []; - } - - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node->var->var, $scope); - if ($propertyReflection === null) { - return []; - } - - $assignedToType = $propertyReflection->getWritableType(); - if (!$assignedToType->isArray()->yes()) { - return []; - } - - if ($node instanceof Assign || $node instanceof AssignRef) { - $assignedValueType = $scope->getType($node->expr); - } else { - $assignedValueType = $scope->getType($node); - } - - $itemType = $assignedToType->getIterableValueType(); - $accepts = $this->ruleLevelHelper->acceptsWithReason($itemType, $assignedValueType, $scope->isDeclareStrictTypes()); - if (!$accepts->result) { - $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($itemType, $assignedValueType); - return [ - RuleErrorBuilder::message(sprintf( - 'Array (%s) does not accept %s.', - $assignedToType->describe($verbosityLevel), - $assignedValueType->describe($verbosityLevel), - )) - ->acceptsReasonsTip($accepts->reasons) - ->identifier('array.valueType') - ->build(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Arrays/AppendedArrayKeyTypeRule.php b/src/Rules/Arrays/AppendedArrayKeyTypeRule.php deleted file mode 100644 index cf35062faf..0000000000 --- a/src/Rules/Arrays/AppendedArrayKeyTypeRule.php +++ /dev/null @@ -1,89 +0,0 @@ - - */ -class AppendedArrayKeyTypeRule implements Rule -{ - - public function __construct( - private PropertyReflectionFinder $propertyReflectionFinder, - private bool $checkUnionTypes, - ) - { - } - - public function getNodeType(): string - { - return Assign::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if (!($node->var instanceof ArrayDimFetch)) { - return []; - } - - if ( - !$node->var->var instanceof Node\Expr\PropertyFetch - && !$node->var->var instanceof Node\Expr\StaticPropertyFetch - ) { - return []; - } - - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node->var->var, $scope); - if ($propertyReflection === null) { - return []; - } - - $arrayType = $propertyReflection->getReadableType(); - if (!$arrayType->isArray()->yes()) { - 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 = $dimensionType->toArrayKey(); - 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()), - ))->identifier('array.keyType')->build(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Arrays/ArrayDestructuringRule.php b/src/Rules/Arrays/ArrayDestructuringRule.php index 332d192698..eac1a12956 100644 --- a/src/Rules/Arrays/ArrayDestructuringRule.php +++ b/src/Rules/Arrays/ArrayDestructuringRule.php @@ -7,6 +7,7 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\Assign; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; @@ -23,7 +24,8 @@ /** * @implements Rule */ -class ArrayDestructuringRule implements Rule +#[RegisteredRule(level: 3)] +final class ArrayDestructuringRule implements Rule { public function __construct( @@ -40,7 +42,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->var instanceof Node\Expr\List_ && !$node->var instanceof Node\Expr\Array_) { + if (!$node->var instanceof Node\Expr\List_) { return []; } @@ -52,10 +54,9 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param Node\Expr\List_|Node\Expr\Array_ $var * @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, @@ -86,7 +87,7 @@ private function getErrors(Scope $scope, Expr $var, Expr $expr): array $keyExpr = null; if ($item->key === null) { $keyType = new ConstantIntegerType($i); - $keyExpr = new Node\Scalar\LNumber($i); + $keyExpr = new Node\Scalar\Int_($i); } else { $keyType = $scope->getType($item->key); $keyExpr = new TypeExpr($keyType); @@ -100,7 +101,7 @@ private function getErrors(Scope $scope, Expr $var, Expr $expr): array ); $errors = array_merge($errors, $itemErrors); - if (!$item->value instanceof Node\Expr\List_ && !$item->value instanceof Node\Expr\Array_) { + if (!$item->value instanceof Node\Expr\List_) { $i++; continue; } diff --git a/src/Rules/Arrays/ArrayUnpackingRule.php b/src/Rules/Arrays/ArrayUnpackingRule.php index 21092c2100..4dc25092a9 100644 --- a/src/Rules/Arrays/ArrayUnpackingRule.php +++ b/src/Rules/Arrays/ArrayUnpackingRule.php @@ -3,8 +3,9 @@ namespace PHPStan\Rules\Arrays; use PhpParser\Node; -use PhpParser\Node\Expr\ArrayItem; +use PhpParser\Node\ArrayItem; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; @@ -18,7 +19,8 @@ /** * @implements Rule */ -class ArrayUnpackingRule implements Rule +#[RegisteredRule(level: 3)] +final class ArrayUnpackingRule implements Rule { public function __construct(private PhpVersion $phpVersion, private RuleLevelHelper $ruleLevelHelper) diff --git a/src/Rules/Arrays/DeadForeachRule.php b/src/Rules/Arrays/DeadForeachRule.php index a9b64b441f..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 Rule */ -class DeadForeachRule implements Rule +#[RegisteredRule(level: 4)] +final class DeadForeachRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php index a1eaf7e131..c05440f892 100644 --- a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php +++ b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php @@ -4,15 +4,18 @@ 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\Constant\ConstantIntegerType; -use PHPStan\Type\ConstantScalarType; +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; @@ -20,7 +23,8 @@ /** * @implements Rule */ -class DuplicateKeysInLiteralArraysRule implements Rule +#[RegisteredRule(level: 0)] +final class DuplicateKeysInLiteralArraysRule implements Rule { public function __construct( @@ -36,7 +40,6 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $values = []; $duplicateKeys = []; $printedValues = []; $valueLines = []; @@ -49,6 +52,8 @@ public function processNode(Node $node, Scope $scope): array * - 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) { @@ -70,38 +75,74 @@ public function processNode(Node $node, Scope $scope): array } } else { $keyType = $itemNode->getScope()->getType($key); - - $arrayKeyValue = $keyType->toArrayKey(); - if ($arrayKeyValue instanceof ConstantIntegerType) { + $arrayKeyValues = $keyType->toArrayKey()->getConstantScalarValues(); + if (count($arrayKeyValues) === 1 && is_int($arrayKeyValues[0])) { $autoGeneratedIndex = $autoGeneratedIndex === null - ? $arrayKeyValue->getValue() - : max($autoGeneratedIndex, $arrayKeyValue->getValue()); + ? $arrayKeyValues[0] + : max($autoGeneratedIndex, $arrayKeyValues[0]); } } - if (!$keyType instanceof ConstantScalarType) { + $keyValues = $keyType->toArrayKey()->getConstantScalarValues(); + if (count($keyValues) === 0) { $autoGeneratedIndex = false; continue; } - $value = $keyType->getValue(); - $printedValue = $key !== null - ? $this->exprPrinter->printExpr($key) - : $value; - - $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->getStartLine(); + 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 = []; diff --git a/src/Rules/Arrays/EmptyArrayItemRule.php b/src/Rules/Arrays/EmptyArrayItemRule.php deleted file mode 100644 index 94cb7e49b0..0000000000 --- a/src/Rules/Arrays/EmptyArrayItemRule.php +++ /dev/null @@ -1,41 +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() - ->identifier('array.emptyItem') - ->build(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php index 1242f85cfd..30f7febe49 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php @@ -4,6 +4,8 @@ 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; @@ -16,11 +18,13 @@ /** * @implements Rule */ -class InvalidKeyInArrayDimFetchRule implements Rule +#[RegisteredRule(level: 3)] +final class InvalidKeyInArrayDimFetchRule implements Rule { public function __construct( private RuleLevelHelper $ruleLevelHelper, + #[AutowiredParameter] private bool $reportMaybes, ) { diff --git a/src/Rules/Arrays/InvalidKeyInArrayItemRule.php b/src/Rules/Arrays/InvalidKeyInArrayItemRule.php index 61c4fd342c..993a6dde61 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayItemRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayItemRule.php @@ -4,6 +4,8 @@ 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; @@ -11,18 +13,22 @@ use function sprintf; /** - * @implements Rule + * @implements Rule */ -class InvalidKeyInArrayItemRule implements Rule +#[RegisteredRule(level: 3)] +final class InvalidKeyInArrayItemRule implements Rule { - public function __construct(private bool $reportMaybes) + public function __construct( + #[AutowiredParameter] + private bool $reportMaybes, + ) { } public function getNodeType(): string { - return Node\Expr\ArrayItem::class; + return Node\ArrayItem::class; } public function processNode(Node $node, Scope $scope): array diff --git a/src/Rules/Arrays/IterableInForeachRule.php b/src/Rules/Arrays/IterableInForeachRule.php index c455cd8d21..2e4204b35b 100644 --- a/src/Rules/Arrays/IterableInForeachRule.php +++ b/src/Rules/Arrays/IterableInForeachRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InForeachNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -16,7 +17,8 @@ /** * @implements Rule */ -class IterableInForeachRule implements Rule +#[RegisteredRule(level: 3)] +final class IterableInForeachRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php index 6ef401c577..872211ec24 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php @@ -5,6 +5,8 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -14,16 +16,20 @@ use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function count; use function sprintf; -class NonexistentOffsetInArrayDimFetchCheck +#[AutowiredService] +final class NonexistentOffsetInArrayDimFetchCheck { public function __construct( private RuleLevelHelper $ruleLevelHelper, + #[AutowiredParameter] private bool $reportMaybes, - private bool $bleedingEdge, + #[AutowiredParameter] private bool $reportPossiblyNonexistentGeneralArrayOffset, + #[AutowiredParameter] private bool $reportPossiblyNonexistentConstantArrayOffset, ) { @@ -56,7 +62,7 @@ public function check( if ($type->hasOffsetValueType($dimType)->no()) { return [ - RuleErrorBuilder::message(sprintf('Offset %s does not exist on %s.', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value()))) + 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(), ]; @@ -104,15 +110,8 @@ public function check( } if ($report) { - if ($this->bleedingEdge || $this->reportPossiblyNonexistentGeneralArrayOffset || $this->reportPossiblyNonexistentConstantArrayOffset) { - return [ - RuleErrorBuilder::message(sprintf('Offset %s might not exist on %s.', $dimType->describe(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()))) + 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(), ]; diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php index 547e2f4fe4..92a03b36bf 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php @@ -5,6 +5,8 @@ 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\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -12,17 +14,22 @@ use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function count; +use function in_array; +use function is_string; use function sprintf; /** * @implements Rule */ -class NonexistentOffsetInArrayDimFetchRule implements Rule +#[RegisteredRule(level: 3)] +final class NonexistentOffsetInArrayDimFetchRule implements Rule { public function __construct( private RuleLevelHelper $ruleLevelHelper, private NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck, + #[AutowiredParameter] private bool $reportMaybes, ) { @@ -74,7 +81,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( 'Cannot access offset %s on %s.', - $dimType->describe(VerbosityLevel::value()), + $dimType->describe(count($dimType->getConstantStrings()) > 0 ? VerbosityLevel::precise() : VerbosityLevel::value()), $isOffsetAccessibleType->describe(VerbosityLevel::value()), ))->identifier('offsetAccess.nonOffsetAccessible')->build(), ]; @@ -95,6 +102,49 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ( + $node->dim instanceof Node\Expr\FuncCall + && $node->dim->name instanceof Node\Name + && in_array($node->dim->name->toLowerString(), ['array_key_first', 'array_key_last'], true) + && count($node->dim->getArgs()) >= 1 + ) { + $arrayArg = $node->dim->getArgs()[0]->value; + $arrayType = $scope->getType($arrayArg); + if ( + $arrayArg instanceof Node\Expr\Variable + && $node->var instanceof Node\Expr\Variable + && is_string($arrayArg->name) + && $arrayArg->name === $node->var->name + && $arrayType->isArray()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + ) { + return []; + } + } + + if ( + $node->dim instanceof Node\Expr\BinaryOp\Minus + && $node->dim->left instanceof Node\Expr\FuncCall + && $node->dim->left->name instanceof Node\Name + && in_array($node->dim->left->name->toLowerString(), ['count', 'sizeof'], true) + && count($node->dim->left->getArgs()) >= 1 + && $node->dim->right instanceof Node\Scalar\Int_ + && $node->dim->right->value === 1 + ) { + $arrayArg = $node->dim->left->getArgs()[0]->value; + $arrayType = $scope->getType($arrayArg); + if ( + $arrayArg instanceof Node\Expr\Variable + && $node->var instanceof Node\Expr\Variable + && is_string($arrayArg->name) + && $arrayArg->name === $node->var->name + && $arrayType->isList()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + ) { + return []; + } + } + return $this->nonexistentOffsetInArrayDimFetchCheck->check( $scope, $node->var, diff --git a/src/Rules/Arrays/OffsetAccessAssignOpRule.php b/src/Rules/Arrays/OffsetAccessAssignOpRule.php index ff66d9bb9f..466d5cd5c8 100644 --- a/src/Rules/Arrays/OffsetAccessAssignOpRule.php +++ b/src/Rules/Arrays/OffsetAccessAssignOpRule.php @@ -5,6 +5,7 @@ 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; @@ -17,7 +18,8 @@ /** * @implements Rule */ -class OffsetAccessAssignOpRule implements Rule +#[RegisteredRule(level: 3)] +final class OffsetAccessAssignOpRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) diff --git a/src/Rules/Arrays/OffsetAccessAssignmentRule.php b/src/Rules/Arrays/OffsetAccessAssignmentRule.php index 0bafe975d0..838636feb6 100644 --- a/src/Rules/Arrays/OffsetAccessAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessAssignmentRule.php @@ -5,6 +5,7 @@ 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; @@ -17,7 +18,8 @@ /** * @implements Rule */ -class OffsetAccessAssignmentRule implements Rule +#[RegisteredRule(level: 3)] +final class OffsetAccessAssignmentRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) diff --git a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php index 0368895760..3d21a95970 100644 --- a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php @@ -7,6 +7,7 @@ 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; @@ -19,7 +20,8 @@ /** * @implements Rule */ -class OffsetAccessValueAssignmentRule implements Rule +#[RegisteredRule(level: 3)] +final class OffsetAccessValueAssignmentRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) @@ -46,6 +48,10 @@ public function processNode(Node $node, Scope $scope): array } $arrayDimFetch = $node->var; + $varType = $scope->getType($arrayDimFetch->var); + if ($varType->isObject()->no()) { + return []; + } if ($node instanceof Assign || $node instanceof Expr\AssignRef) { $assignedValueType = $scope->getType($node->expr); diff --git a/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php b/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php index ce70e82425..b48a5b8966 100644 --- a/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php +++ b/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.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 OffsetAccessWithoutDimForReadingRule implements Rule +#[RegisteredRule(level: 0)] +final class OffsetAccessWithoutDimForReadingRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Arrays/UnpackIterableInArrayRule.php b/src/Rules/Arrays/UnpackIterableInArrayRule.php index 10a3a67758..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; @@ -16,7 +17,8 @@ /** * @implements Rule */ -class UnpackIterableInArrayRule implements Rule +#[RegisteredRule(level: 3)] +final class UnpackIterableInArrayRule implements Rule { public function __construct( diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index 916c6e1c7d..36b3c6faa6 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -6,6 +6,8 @@ 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; @@ -14,13 +16,15 @@ use function sprintf; use function strtolower; -class AttributesCheck +#[AutowiredService] +final class AttributesCheck { public function __construct( private ReflectionProvider $reflectionProvider, private FunctionCallParametersCheck $functionCallParametersCheck, private ClassNameCheck $classCheck, + #[AutowiredParameter] private bool $deprecationRulesInstalled, ) { @@ -67,7 +71,7 @@ public function check( ->build(); } - foreach ($this->classCheck->checkClassNames([new ClassNameNodePair($name, $attribute)]) as $caseSensitivityError) { + foreach ($this->classCheck->checkClassNames($scope, [new ClassNameNodePair($name, $attribute)], ClassNameUsageLocation::from(ClassNameUsageLocation::ATTRIBUTE)) as $caseSensitivityError) { $errors[] = $caseSensitivityError; } @@ -136,23 +140,23 @@ public function check( $scope, $attributeConstructor->getDeclaringClass()->isBuiltin(), new New_($attribute->name, $attribute->args, $nodeAttributes), - [ - 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, %d required.', - 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, %d required.', - 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, at least %d required.', - 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, at least %d required.', - 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, %d-%d required.', - 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, %d-%d required.', - 'Parameter %s of attribute class ' . $attributeClassName . ' constructor expects %s, %s given.', - '', // constructor does not have a return type - 'Parameter %s of attribute class ' . $attributeClassName . ' constructor is passed by reference, so it expects variables only', - 'Unable to resolve the template type %s in instantiation of attribute class ' . $attributeClassName, - 'Missing parameter $%s in call to ' . $attributeClassName . ' constructor.', - 'Unknown parameter $%s in call to ' . $attributeClassName . ' constructor.', - 'Return type of call to ' . $attributeClassName . ' constructor contains unresolvable type.', - 'Parameter %s of attribute class ' . $attributeClassName . ' constructor contains unresolvable type.', - ], 'attribute', + $attributeConstructor->acceptsNamedArguments(), + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, %d required.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, %d required.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, at least %d required.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, at least %d required.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, %d-%d required.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, %d-%d required.', + '%s of attribute class ' . $attributeClassName . ' constructor expects %s, %s given.', + '', // constructor does not have a return type + '%s of attribute class ' . $attributeClassName . ' constructor is passed by reference, so it expects variables only', + 'Unable to resolve the template type %s in instantiation of attribute class ' . $attributeClassName, + 'Missing parameter $%s in call to ' . $attributeClassName . ' constructor.', + 'Unknown parameter $%s in call to ' . $attributeClassName . ' constructor.', + 'Return type of call to ' . $attributeClassName . ' constructor contains unresolvable type.', + '%s of attribute class ' . $attributeClassName . ' constructor contains unresolvable type.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %s, but it\'s not allowed because of @no-named-arguments.', ); foreach ($parameterErrors as $error) { diff --git a/src/Rules/Cast/EchoRule.php b/src/Rules/Cast/EchoRule.php index d8033f5394..4844d1e0a0 100644 --- a/src/Rules/Cast/EchoRule.php +++ b/src/Rules/Cast/EchoRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -15,7 +16,8 @@ /** * @implements Rule */ -class EchoRule implements Rule +#[RegisteredRule(level: 2)] +final class EchoRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) diff --git a/src/Rules/Cast/InvalidCastRule.php b/src/Rules/Cast/InvalidCastRule.php index ceb074e36c..38b97e396a 100644 --- a/src/Rules/Cast/InvalidCastRule.php +++ b/src/Rules/Cast/InvalidCastRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -19,7 +20,8 @@ /** * @implements Rule */ -class InvalidCastRule implements Rule +#[RegisteredRule(level: 2)] +final class InvalidCastRule implements Rule { public function __construct( diff --git a/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php b/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php index 773aabe5d5..d8d899e72d 100644 --- a/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php +++ b/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,9 +15,10 @@ use function sprintf; /** - * @implements Rule + * @implements Rule */ -class InvalidPartOfEncapsedStringRule implements Rule +#[RegisteredRule(level: 2)] +final class InvalidPartOfEncapsedStringRule implements Rule { public function __construct( @@ -28,14 +30,14 @@ public function __construct( public function getNodeType(): string { - return Node\Scalar\Encapsed::class; + return Node\Scalar\InterpolatedString::class; } public function processNode(Node $node, Scope $scope): array { $messages = []; foreach ($node->parts as $part) { - if ($part instanceof Node\Scalar\EncapsedStringPart) { + if ($part instanceof Node\InterpolatedStringPart) { continue; } diff --git a/src/Rules/Cast/PrintRule.php b/src/Rules/Cast/PrintRule.php index 3b8ad5f97d..8e881f8ba3 100644 --- a/src/Rules/Cast/PrintRule.php +++ b/src/Rules/Cast/PrintRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -15,7 +16,8 @@ /** * @implements Rule */ -class PrintRule implements Rule +#[RegisteredRule(level: 2)] +final class PrintRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) diff --git a/src/Rules/Cast/UnsetCastRule.php b/src/Rules/Cast/UnsetCastRule.php index be527ffad0..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,7 +12,8 @@ /** * @implements Rule */ -class UnsetCastRule implements Rule +#[RegisteredRule(level: 0)] +final class UnsetCastRule implements Rule { public function __construct(private PhpVersion $phpVersion) diff --git a/src/Rules/ClassCaseSensitivityCheck.php b/src/Rules/ClassCaseSensitivityCheck.php index f7cdfdaab3..bec217c30f 100644 --- a/src/Rules/ClassCaseSensitivityCheck.php +++ b/src/Rules/ClassCaseSensitivityCheck.php @@ -2,14 +2,21 @@ namespace PHPStan\Rules; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ReflectionProvider; use function sprintf; use function strtolower; -class ClassCaseSensitivityCheck +#[AutowiredService] +final class ClassCaseSensitivityCheck { - public function __construct(private ReflectionProvider $reflectionProvider, private bool $checkInternalClassCaseSensitivity) + public function __construct( + private ReflectionProvider $reflectionProvider, + #[AutowiredParameter] + private bool $checkInternalClassCaseSensitivity, + ) { } diff --git a/src/Rules/ClassForbiddenNameCheck.php b/src/Rules/ClassForbiddenNameCheck.php index c86fede587..70249cc666 100644 --- a/src/Rules/ClassForbiddenNameCheck.php +++ b/src/Rules/ClassForbiddenNameCheck.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; use function array_map; use function array_merge; @@ -12,7 +13,8 @@ use function strpos; use function substr; -class ClassForbiddenNameCheck +#[AutowiredService] +final class ClassForbiddenNameCheck { private const INTERNAL_CLASS_PREFIXES = [ @@ -57,11 +59,12 @@ public function checkClassNames(array $pairs): array $projectName = $project; $withoutPrefixClassName = substr($className, strlen($prefix)); - if (strpos($withoutPrefixClassName, '\\') === false) { + $pos = strpos($withoutPrefixClassName, '\\'); + if ($pos === false) { continue; } - $withoutPrefixClassName = substr($withoutPrefixClassName, strpos($withoutPrefixClassName, '\\')); + $withoutPrefixClassName = substr($withoutPrefixClassName, $pos); } if ($projectName === null) { @@ -73,7 +76,7 @@ public function checkClassNames(array $pairs): array $projectName, $className, )) - ->line($pair->getNode()->getLine()) + ->line($pair->getNode()->getStartLine()) ->identifier('class.prefixed') ->nonIgnorable(); diff --git a/src/Rules/ClassNameCheck.php b/src/Rules/ClassNameCheck.php index c168b74ce7..ba23578483 100644 --- a/src/Rules/ClassNameCheck.php +++ b/src/Rules/ClassNameCheck.php @@ -2,12 +2,21 @@ namespace PHPStan\Rules; -class ClassNameCheck +use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\DependencyInjection\Container; +use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\RestrictedUsage\RestrictedClassNameUsageExtension; + +#[AutowiredService] +final class ClassNameCheck { public function __construct( private ClassCaseSensitivityCheck $classCaseSensitivityCheck, private ClassForbiddenNameCheck $classForbiddenNameCheck, + private ReflectionProvider $reflectionProvider, + private Container $container, ) { } @@ -16,7 +25,12 @@ public function __construct( * @param ClassNameNodePair[] $pairs * @return list */ - public function checkClassNames(array $pairs, bool $checkClassCaseSensitivity = true): array + public function checkClassNames( + Scope $scope, + array $pairs, + ?ClassNameUsageLocation $location, + bool $checkClassCaseSensitivity = true, + ): array { $errors = []; @@ -29,6 +43,35 @@ public function checkClassNames(array $pairs, bool $checkClassCaseSensitivity = $errors[] = $error; } + if ($location === null) { + return $errors; + } + + /** @var RestrictedClassNameUsageExtension[] $extensions */ + $extensions = $this->container->getServicesByTag(RestrictedClassNameUsageExtension::CLASS_NAME_EXTENSION_TAG); + if ($extensions === []) { + return $errors; + } + + foreach ($pairs as $pair) { + if (!$this->reflectionProvider->hasClass($pair->getClassName())) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($pair->getClassName()); + foreach ($extensions as $extension) { + $restrictedUsage = $extension->isRestrictedClassNameUsage($classReflection, $scope, $location); + if ($restrictedUsage === null) { + continue; + } + + $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier) + ->line($pair->getNode()->getStartLine()) + ->build(); + } + } + return $errors; } diff --git a/src/Rules/ClassNameNodePair.php b/src/Rules/ClassNameNodePair.php index 355fa61e01..cf39baa35c 100644 --- a/src/Rules/ClassNameNodePair.php +++ b/src/Rules/ClassNameNodePair.php @@ -4,7 +4,7 @@ use PhpParser\Node; -class ClassNameNodePair +final class ClassNameNodePair { public function __construct(private string $className, private Node $node) diff --git a/src/Rules/ClassNameUsageLocation.php b/src/Rules/ClassNameUsageLocation.php new file mode 100644 index 0000000000..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 438398a4f9..ef86b84686 100644 --- a/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php +++ b/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php @@ -5,6 +5,7 @@ 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; @@ -12,7 +13,8 @@ /** * @implements Rule */ -class AccessPrivateConstantThroughStaticRule implements Rule +#[RegisteredRule(level: 2)] +final class AccessPrivateConstantThroughStaticRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Classes/AllowedSubTypesRule.php b/src/Rules/Classes/AllowedSubTypesRule.php index 164e63d9d7..b5fc2a7b32 100644 --- a/src/Rules/Classes/AllowedSubTypesRule.php +++ b/src/Rules/Classes/AllowedSubTypesRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,7 +15,8 @@ /** * @implements Rule */ -class AllowedSubTypesRule implements Rule +#[RegisteredRule(level: 0)] +final class AllowedSubTypesRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Classes/ClassAttributesRule.php b/src/Rules/Classes/ClassAttributesRule.php index 21d6bda749..7bce7b065c 100644 --- a/src/Rules/Classes/ClassAttributesRule.php +++ b/src/Rules/Classes/ClassAttributesRule.php @@ -5,13 +5,19 @@ 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 { public function __construct(private AttributesCheck $attributesCheck) @@ -20,17 +26,46 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Stmt\ClassLike::class; + return InClassNode::class; } public function processNode(Node $node, Scope $scope): array { - return $this->attributesCheck->check( + $classLikeNode = $node->getOriginalNode(); + + $errors = $this->attributesCheck->check( $scope, - $node->attrGroups, + $classLikeNode->attrGroups, Attribute::TARGET_CLASS, 'class', ); + + $classReflection = $node->getClassReflection(); + if ( + $classReflection->isReadOnly() + || $classReflection->isEnum() + || $classReflection->isInterface() + ) { + $typeName = 'readonly class'; + $identifier = 'class.allowDynamicPropertiesReadonly'; + if ($classReflection->isEnum()) { + $typeName = 'enum'; + $identifier = 'enum.allowDynamicProperties'; + } + if ($classReflection->isInterface()) { + $typeName = 'interface'; + $identifier = 'interface.allowDynamicProperties'; + } + + if (count($classReflection->getNativeReflection()->getAttributes('AllowDynamicProperties')) > 0) { + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class AllowDynamicProperties cannot be used with %s.', $typeName)) + ->identifier($identifier) + ->nonIgnorable() + ->build(); + } + } + + return $errors; } } diff --git a/src/Rules/Classes/ClassConstantAttributesRule.php b/src/Rules/Classes/ClassConstantAttributesRule.php index d8256191a0..3beaf3d0ea 100644 --- a/src/Rules/Classes/ClassConstantAttributesRule.php +++ b/src/Rules/Classes/ClassConstantAttributesRule.php @@ -5,13 +5,15 @@ 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 { public function __construct(private AttributesCheck $attributesCheck) diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index f8425ff295..a8220d5e11 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -3,14 +3,21 @@ 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\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -28,7 +35,8 @@ /** * @implements Rule */ -class ClassConstantRule implements Rule +#[RegisteredRule(level: 0)] +final class ClassConstantRule implements Rule { public function __construct( @@ -36,6 +44,8 @@ public function __construct( private RuleLevelHelper $ruleLevelHelper, private ClassNameCheck $classCheck, private PhpVersion $phpVersion, + #[AutowiredParameter(ref: '%featureToggles.checkNonStringableDynamicAccess%')] + private bool $checkNonStringableDynamicAccess, ) { } @@ -47,11 +57,54 @@ 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 Node\Name) { @@ -111,9 +164,34 @@ public function processNode(Node $node, Scope $scope): array ]; } - $messages = $this->classCheck->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') { diff --git a/src/Rules/Classes/DuplicateClassDeclarationRule.php b/src/Rules/Classes/DuplicateClassDeclarationRule.php index ae2658de79..bf8ac7f05d 100644 --- a/src/Rules/Classes/DuplicateClassDeclarationRule.php +++ b/src/Rules/Classes/DuplicateClassDeclarationRule.php @@ -20,7 +20,7 @@ /** * @implements Rule */ -class DuplicateClassDeclarationRule implements Rule +final class DuplicateClassDeclarationRule implements Rule { public function __construct(private Reflector $reflector, private RelativePathHelper $relativePathHelper) diff --git a/src/Rules/Classes/DuplicateDeclarationRule.php b/src/Rules/Classes/DuplicateDeclarationRule.php index b5e122eb0e..6fe86ed9bc 100644 --- a/src/Rules/Classes/DuplicateDeclarationRule.php +++ b/src/Rules/Classes/DuplicateDeclarationRule.php @@ -6,6 +6,7 @@ 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; @@ -18,7 +19,8 @@ /** * @implements Rule */ -class DuplicateDeclarationRule implements Rule +#[RegisteredRule(level: 0)] +final class DuplicateDeclarationRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Classes/EnumSanityRule.php b/src/Rules/Classes/EnumSanityRule.php index 742dbd1842..fc5d56ba8a 100644 --- a/src/Rules/Classes/EnumSanityRule.php +++ b/src/Rules/Classes/EnumSanityRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -20,7 +21,8 @@ /** * @implements Rule */ -class EnumSanityRule implements Rule +#[RegisteredRule(level: 0)] +final class EnumSanityRule implements Rule { private const ALLOWED_MAGIC_METHODS = [ @@ -47,20 +49,7 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($enumNode->getMethods() as $methodNode) { - if ($methodNode->isAbstract()) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'Enum %s contains abstract method %s().', - $classReflection->getDisplayName(), - $methodNode->name->name, - )) - ->identifier('enum.abstractMethod') - ->line($methodNode->getStartLine()) - ->nonIgnorable() - ->build(); - } - $lowercasedMethodName = $methodNode->name->toLowerString(); - if ($methodNode->isMagic()) { if ($lowercasedMethodName === '__construct') { $errors[] = RuleErrorBuilder::message(sprintf( @@ -156,7 +145,7 @@ public function processNode(Node $node, Scope $scope): array } $caseName = $stmt->name->name; - if ($stmt->expr instanceof Node\Scalar\LNumber || $stmt->expr instanceof Node\Scalar\String_) { + 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.', diff --git a/src/Rules/Classes/ExistingClassInClassExtendsRule.php b/src/Rules/Classes/ExistingClassInClassExtendsRule.php index b0a6aae26d..4dd7daf602 100644 --- a/src/Rules/Classes/ExistingClassInClassExtendsRule.php +++ b/src/Rules/Classes/ExistingClassInClassExtendsRule.php @@ -4,9 +4,12 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; @@ -14,12 +17,15 @@ /** * @implements Rule */ -class ExistingClassInClassExtendsRule implements Rule +#[RegisteredRule(level: 0)] +final class ExistingClassInClassExtendsRule implements Rule { public function __construct( private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { } @@ -35,22 +41,33 @@ public function processNode(Node $node, Scope $scope): array return []; } $extendedClassName = (string) $node->extends; - $messages = $this->classCheck->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, )) ->identifier('class.notFound') - ->nonIgnorable() - ->discoveringSymbolsTip() - ->build(); + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); } } else { $reflection = $this->reflectionProvider->getClass($extendedClassName); diff --git a/src/Rules/Classes/ExistingClassInInstanceOfRule.php b/src/Rules/Classes/ExistingClassInInstanceOfRule.php index 7eef8ba523..1ad6e48671 100644 --- a/src/Rules/Classes/ExistingClassInInstanceOfRule.php +++ b/src/Rules/Classes/ExistingClassInInstanceOfRule.php @@ -5,9 +5,12 @@ 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\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; @@ -19,13 +22,17 @@ /** * @implements Rule */ -class ExistingClassInInstanceOfRule implements Rule +#[RegisteredRule(level: 0)] +final class ExistingClassInInstanceOfRule implements Rule { public function __construct( private ReflectionProvider $reflectionProvider, private ClassNameCheck $classCheck, + #[AutowiredParameter] private bool $checkClassCaseSensitivity, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { } @@ -69,19 +76,25 @@ public function processNode(Node $node, Scope $scope): array 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)) - ->identifier('class.notFound') - ->line($class->getStartLine()) - ->discoveringSymbolsTip() - ->build(), + $errorBuilder->build(), ]; } $errors = array_merge( $errors, $this->classCheck->checkClassNames( + $scope, [new ClassNameNodePair($name, $class)], + ClassNameUsageLocation::from(ClassNameUsageLocation::INSTANCEOF), $this->checkClassCaseSensitivity, ), ); diff --git a/src/Rules/Classes/ExistingClassInTraitUseRule.php b/src/Rules/Classes/ExistingClassInTraitUseRule.php index e47d9cf5d6..a624081a8f 100644 --- a/src/Rules/Classes/ExistingClassInTraitUseRule.php +++ b/src/Rules/Classes/ExistingClassInTraitUseRule.php @@ -4,9 +4,12 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; @@ -16,12 +19,15 @@ /** * @implements Rule */ -class ExistingClassInTraitUseRule implements Rule +#[RegisteredRule(level: 0)] +final class ExistingClassInTraitUseRule implements Rule { public function __construct( private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { } @@ -33,15 +39,20 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $messages = $this->classCheck->checkClassNames( - array_map(static fn (Node\Name $traitName): ClassNameNodePair => new ClassNameNodePair((string) $traitName, $traitName), $node->traits), - ); - if (!$scope->isInClass()) { throw new ShouldNotHappenException(); } $classReflection = $scope->getClassReflection(); + + $messages = $this->classCheck->checkClassNames( + $scope, + array_map(static fn (Node\Name $traitName): ClassNameNodePair => new ClassNameNodePair((string) $traitName, $traitName), $node->traits), + ClassNameUsageLocation::from(ClassNameUsageLocation::TRAIT_USE, [ + 'currentClassName' => $classReflection->isAnonymous() ? null : $classReflection->getName(), + ]), + ); + if ($classReflection->isInterface()) { if (!$scope->isInTrait()) { foreach ($node->traits as $trait) { @@ -64,11 +75,15 @@ 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)) + $errorBuilder = RuleErrorBuilder::message(sprintf('%s uses unknown trait %s.', $currentName, $traitName)) ->identifier('trait.notFound') - ->nonIgnorable() - ->discoveringSymbolsTip() - ->build(); + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); } else { $reflection = $this->reflectionProvider->getClass($traitName); if ($reflection->isClass()) { diff --git a/src/Rules/Classes/ExistingClassesInClassImplementsRule.php b/src/Rules/Classes/ExistingClassesInClassImplementsRule.php index 5aeb3fde46..8d270048ed 100644 --- a/src/Rules/Classes/ExistingClassesInClassImplementsRule.php +++ b/src/Rules/Classes/ExistingClassesInClassImplementsRule.php @@ -4,9 +4,12 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function array_map; @@ -15,12 +18,15 @@ /** * @implements Rule */ -class ExistingClassesInClassImplementsRule implements Rule +#[RegisteredRule(level: 0)] +final class ExistingClassesInClassImplementsRule implements Rule { public function __construct( private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { } @@ -32,28 +38,36 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $messages = $this->classCheck->checkClassNames( - array_map(static fn (Node\Name $interfaceName): ClassNameNodePair => new ClassNameNodePair((string) $interfaceName, $interfaceName), $node->implements), - ); - $currentClassName = null; if (isset($node->namespacedName)) { $currentClassName = (string) $node->namespacedName; } + $messages = $this->classCheck->checkClassNames( + $scope, + array_map(static fn (Node\Name $interfaceName): ClassNameNodePair => new ClassNameNodePair((string) $interfaceName, $interfaceName), $node->implements), + ClassNameUsageLocation::from(ClassNameUsageLocation::CLASS_IMPLEMENTS, [ + 'currentClassName' => $currentClassName, + ]), + ); + foreach ($node->implements as $implements) { $implementedClassName = (string) $implements; if (!$this->reflectionProvider->hasClass($implementedClassName)) { if (!$scope->isInClassExists($implementedClassName)) { - $messages[] = RuleErrorBuilder::message(sprintf( + $errorBuilder = RuleErrorBuilder::message(sprintf( '%s implements unknown interface %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $implementedClassName, )) ->identifier('interface.notFound') - ->nonIgnorable() - ->discoveringSymbolsTip() - ->build(); + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); } } else { $reflection = $this->reflectionProvider->getClass($implementedClassName); diff --git a/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php b/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php index 413f32fcd8..05d830cd16 100644 --- a/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php +++ b/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php @@ -4,9 +4,12 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function array_map; @@ -15,12 +18,15 @@ /** * @implements Rule */ -class ExistingClassesInEnumImplementsRule implements Rule +#[RegisteredRule(level: 0)] +final class ExistingClassesInEnumImplementsRule implements Rule { public function __construct( private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { } @@ -32,25 +38,32 @@ public function getNodeType(): string 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, + ]), ); - $currentEnumName = (string) $node->namespacedName; - foreach ($node->implements as $implements) { $implementedClassName = (string) $implements; if (!$this->reflectionProvider->hasClass($implementedClassName)) { if (!$scope->isInClassExists($implementedClassName)) { - $messages[] = RuleErrorBuilder::message(sprintf( + $errorBuilder = RuleErrorBuilder::message(sprintf( 'Enum %s implements unknown interface %s.', $currentEnumName, $implementedClassName, )) ->identifier('interface.notFound') - ->nonIgnorable() - ->discoveringSymbolsTip() - ->build(); + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); } } else { $reflection = $this->reflectionProvider->getClass($implementedClassName); diff --git a/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php b/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php index e25a2ac974..345c3663c1 100644 --- a/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php +++ b/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php @@ -4,9 +4,12 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function array_map; @@ -15,12 +18,15 @@ /** * @implements Rule */ -class ExistingClassesInInterfaceExtendsRule implements Rule +#[RegisteredRule(level: 0)] +final class ExistingClassesInInterfaceExtendsRule implements Rule { public function __construct( private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { } @@ -32,24 +38,32 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { + $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, )) ->identifier('interface.notFound') - ->nonIgnorable() - ->discoveringSymbolsTip() - ->build(); + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); } } else { $reflection = $this->reflectionProvider->getClass($extendedInterfaceName); diff --git a/src/Rules/Classes/ImpossibleInstanceOfRule.php b/src/Rules/Classes/ImpossibleInstanceOfRule.php index c84092f810..84c763638d 100644 --- a/src/Rules/Classes/ImpossibleInstanceOfRule.php +++ b/src/Rules/Classes/ImpossibleInstanceOfRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -18,13 +20,17 @@ /** * @implements Rule */ -class ImpossibleInstanceOfRule implements Rule +#[RegisteredRule(level: 4)] +final class ImpossibleInstanceOfRule implements Rule { public function __construct( - private bool $checkAlwaysTrueInstanceof, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -71,6 +77,10 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; @@ -84,28 +94,26 @@ public function processNode(Node $node, Scope $scope): array $classType->describe(VerbosityLevel::getRecommendedLevelByType($classType)), )))->identifier('instanceof.alwaysFalse')->build(), ]; - } elseif ($this->checkAlwaysTrueInstanceof) { - $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'); + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } - return [$errorBuilder->build()]; + $exprType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->expr) : $scope->getNativeType($node->expr); + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Instanceof between %s and %s will always evaluate to true.', + $exprType->describe(VerbosityLevel::typeOnly()), + $classType->describe(VerbosityLevel::getRecommendedLevelByType($classType)), + ))); + if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); } - return []; + $errorBuilder->identifier('instanceof.alwaysTrue'); + + return [$errorBuilder->build()]; } } diff --git a/src/Rules/Classes/InstantiationCallableRule.php b/src/Rules/Classes/InstantiationCallableRule.php index d36c15a33a..a263c1d49b 100644 --- a/src/Rules/Classes/InstantiationCallableRule.php +++ b/src/Rules/Classes/InstantiationCallableRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InstantiationCallableNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -11,7 +12,8 @@ /** * @implements Rule */ -class InstantiationCallableRule implements Rule +#[RegisteredRule(level: 0)] +final class InstantiationCallableRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index 7e31e5adcd..3b1452acc2 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -5,18 +5,26 @@ 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\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\FunctionCallParametersCheck; 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 function array_filter; use function array_map; use function array_merge; use function count; @@ -26,13 +34,17 @@ /** * @implements Rule */ -class InstantiationRule implements Rule +#[RegisteredRule(level: 0)] +final class InstantiationRule implements Rule { public function __construct( + private Container $container, private ReflectionProvider $reflectionProvider, private FunctionCallParametersCheck $check, private ClassNameCheck $classCheck, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { } @@ -120,17 +132,21 @@ 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)) - ->identifier('class.notFound') - ->discoveringSymbolsTip() - ->build(), + $errorBuilder->build(), ]; } - $messages = $this->classCheck->checkClassNames([ + $messages = $this->classCheck->checkClassNames($scope, [ new ClassNameNodePair($class, $node->class), - ]); + ], ClassNameUsageLocation::from(ClassNameUsageLocation::INSTANTIATION)); $classReflection = $this->reflectionProvider->getClass($class); } @@ -189,6 +205,28 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ ->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()); return array_merge($messages, $this->check->check( @@ -201,23 +239,23 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ $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.', - 'Parameter %s of class ' . $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.', )); } @@ -245,6 +283,20 @@ private function getClassNames(Node $node, Scope $scope): array $type = $scope->getType($node->class); + if ($type->isClassString()->yes()) { + $concretes = array_filter( + $type->getClassStringObjectType()->getObjectClassReflections(), + static fn (ClassReflection $classReflection): bool => !$classReflection->isAbstract() && !$classReflection->isInterface(), + ); + + if (count($concretes) > 0) { + return array_map( + static fn (ClassReflection $classReflection): array => [$classReflection->getName(), true], + $concretes, + ); + } + } + return array_merge( array_map( static fn (ConstantStringType $type): array => [$type->getValue(), true], diff --git a/src/Rules/Classes/InvalidPromotedPropertiesRule.php b/src/Rules/Classes/InvalidPromotedPropertiesRule.php index 5376f6b17e..ef2e2c167a 100644 --- a/src/Rules/Classes/InvalidPromotedPropertiesRule.php +++ b/src/Rules/Classes/InvalidPromotedPropertiesRule.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; @@ -14,7 +15,8 @@ /** * @implements Rule */ -class InvalidPromotedPropertiesRule implements Rule +#[RegisteredRule(level: 0)] +final class InvalidPromotedPropertiesRule implements Rule { public function __construct(private PhpVersion $phpVersion) @@ -31,7 +33,12 @@ public function processNode(Node $node, Scope $scope): array $hasPromotedProperties = false; foreach ($node->getParams() as $param) { - if ($param->flags === 0) { + if ($param->flags !== 0) { + $hasPromotedProperties = true; + break; + } + + if ($param->hooks === []) { continue; } diff --git a/src/Rules/Classes/LocalTypeAliasesCheck.php b/src/Rules/Classes/LocalTypeAliasesCheck.php index 78940cac12..a849ecac8c 100644 --- a/src/Rules/Classes/LocalTypeAliasesCheck.php +++ b/src/Rules/Classes/LocalTypeAliasesCheck.php @@ -2,32 +2,57 @@ namespace PHPStan\Rules\Classes; +use PhpParser\Node\Stmt\ClassLike; use PHPStan\Analyser\NameScope; +use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Internal\SprintfHelper; use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\ClassNameCheck; +use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; +use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\CircularTypeAliasErrorType; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; +use PHPStan\Type\VerbosityLevel; use function array_key_exists; +use function array_merge; use function in_array; use function sprintf; -class LocalTypeAliasesCheck +#[AutowiredService] +final class LocalTypeAliasesCheck { /** * @param array $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, ) { } @@ -35,7 +60,23 @@ public function __construct( /** * @return list */ - public function check(ClassReflection $reflection): array + 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) { @@ -52,7 +93,7 @@ public function check(ClassReflection $reflection): array }; $errors = []; - $className = $reflection->getName(); + $className = $reflection->getDisplayName(); $importedAliases = []; @@ -145,30 +186,143 @@ public function check(ClassReflection $reflection): array } $resolvedType = $typeAliasTag->getTypeAlias()->resolve($this->typeNodeResolver); - $foundError = false; - TypeTraverser::map($resolvedType, static function (Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): Type { - if ($foundError) { - return $type; - } + if ($this->hasErrorType($resolvedType, $aliasName, $errors)) { + continue; + } - 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 (!$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 = []; - if ($type instanceof ErrorType) { - $errors[] = RuleErrorBuilder::message(sprintf('Invalid type definition detected in type alias %s.', $aliasName)) - ->identifier('typeAlias.invalidType') + 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(); - $foundError = true; - return $type; + } else { + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames($scope, [ + new ClassNameNodePair($class, $node), + ], ClassNameUsageLocation::from(ClassNameUsageLocation::TYPE_ALIAS, [ + 'typeAliasName' => $aliasName, + ]), $this->checkClassCaseSensitivity), + ); } + } - return $traverse($type); - }); + 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; @@ -185,4 +339,38 @@ private function isAliasNameValid(string $aliasName, ?NameScope $nameScope): boo || $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 86697971b8..597d4c04c1 100644 --- a/src/Rules/Classes/LocalTypeAliasesRule.php +++ b/src/Rules/Classes/LocalTypeAliasesRule.php @@ -4,13 +4,15 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; /** * @implements Rule */ -class LocalTypeAliasesRule implements Rule +#[RegisteredRule(level: 0)] +final class LocalTypeAliasesRule implements Rule { public function __construct(private LocalTypeAliasesCheck $check) @@ -24,7 +26,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - return $this->check->check($node->getClassReflection()); + return $this->check->check($scope, $node->getClassReflection(), $node->getOriginalNode()); } } diff --git a/src/Rules/Classes/LocalTypeTraitAliasesRule.php b/src/Rules/Classes/LocalTypeTraitAliasesRule.php index 406108db1b..0b8533975a 100644 --- a/src/Rules/Classes/LocalTypeTraitAliasesRule.php +++ b/src/Rules/Classes/LocalTypeTraitAliasesRule.php @@ -4,13 +4,15 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; /** * @implements Rule */ -class LocalTypeTraitAliasesRule implements Rule +#[RegisteredRule(level: 0)] +final class LocalTypeTraitAliasesRule implements Rule { public function __construct(private LocalTypeAliasesCheck $check, private ReflectionProvider $reflectionProvider) @@ -33,7 +35,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - return $this->check->check($this->reflectionProvider->getClass($traitName->toString())); + 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 ce397c3e21..330b403e24 100644 --- a/src/Rules/Classes/MixinRule.php +++ b/src/Rules/Classes/MixinRule.php @@ -4,34 +4,18 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassNameCheck; -use PHPStan\Rules\ClassNameNodePair; -use PHPStan\Rules\Generics\GenericObjectTypeCheck; -use PHPStan\Rules\MissingTypehintCheck; -use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\VerbosityLevel; -use function array_merge; -use function implode; -use function sprintf; /** * @implements Rule */ -class MixinRule implements Rule +#[RegisteredRule(level: 2)] +final class MixinRule implements Rule { - public function __construct( - private ReflectionProvider $reflectionProvider, - private ClassNameCheck $classCheck, - private GenericObjectTypeCheck $genericObjectTypeCheck, - private MissingTypehintCheck $missingTypehintCheck, - private UnresolvableTypeHelper $unresolvableTypeHelper, - private bool $checkClassCaseSensitivity, - ) + public function __construct(private MixinCheck $check) { } @@ -42,69 +26,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $classReflection = $node->getClassReflection(); - $mixinTags = $classReflection->getMixinTags(); - $errors = []; - foreach ($mixinTags as $mixinTag) { - $type = $mixinTag->getType(); - if (!$type->canCallMethods()->yes() || !$type->canAccessProperties()->yes()) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly()))) - ->identifier('mixin.nonObject') - ->build(); - continue; - } - - 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 ($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), - )) - ->identifier('missingType.generics') - ->build(); - } - - foreach ($type->getReferencedClasses() as $class) { - if (!$this->reflectionProvider->hasClass($class)) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains unknown class %s.', $class)) - ->identifier('class.notFound') - ->discoveringSymbolsTip() - ->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([ - new ClassNameNodePair($class, $node), - ], $this->checkClassCaseSensitivity), - ); - } - } - } - - 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 b675469bac..71f682c616 100644 --- a/src/Rules/Classes/NewStaticRule.php +++ b/src/Rules/Classes/NewStaticRule.php @@ -4,17 +4,27 @@ 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 Rule */ -class NewStaticRule implements Rule +#[RegisteredRule(level: 0)] +final class NewStaticRule implements Rule { + public function __construct( + private PhpVersion $phpVersion, + ) + { + } + public function getNodeType(): string { return Node\Expr\New_::class; @@ -75,6 +85,19 @@ public function processNode(Node $node, Scope $scope): array } } + 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 c6318d07a4..7e11b6feed 100644 --- a/src/Rules/Classes/NonClassAttributeClassRule.php +++ b/src/Rules/Classes/NonClassAttributeClassRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; @@ -15,7 +16,8 @@ /** * @implements Rule */ -class NonClassAttributeClassRule implements Rule +#[RegisteredRule(level: 0)] +final class NonClassAttributeClassRule implements Rule { public function getNodeType(): string 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 index 21755c623c..c29315a4ff 100644 --- a/src/Rules/Classes/ReadOnlyClassRule.php +++ b/src/Rules/Classes/ReadOnlyClassRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; @@ -12,7 +13,8 @@ /** * @implements Rule */ -class ReadOnlyClassRule implements Rule +#[RegisteredRule(level: 0)] +final class ReadOnlyClassRule implements Rule { public function __construct(private PhpVersion $phpVersion) diff --git a/src/Rules/Classes/RequireExtendsRule.php b/src/Rules/Classes/RequireExtendsRule.php index 34a35ab11f..29dd6dce68 100644 --- a/src/Rules/Classes/RequireExtendsRule.php +++ b/src/Rules/Classes/RequireExtendsRule.php @@ -4,17 +4,18 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; use function sprintf; /** * @implements Rule */ -class RequireExtendsRule implements Rule +#[RegisteredRule(level: 2)] +final class RequireExtendsRule implements Rule { public function getNodeType(): string @@ -35,24 +36,24 @@ public function processNode(Node $node, Scope $scope): array $extendsTags = $interface->getRequireExtendsTags(); foreach ($extendsTags as $extendsTag) { $type = $extendsTag->getType(); - if (!$type instanceof ObjectType) { - continue; - } + foreach ($type->getObjectClassNames() as $className) { + if ($classReflection->is($className)) { + continue; + } - if ($classReflection->is($type->getClassName())) { - continue; - } + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Interface %s requires implementing class to extend %s, but %s does not.', + $interface->getDisplayName(), + $type->describe(VerbosityLevel::typeOnly()), + $classReflection->getDisplayName(), + ), + ) + ->identifier('class.missingExtends') + ->build(); - $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; + } } } @@ -60,24 +61,24 @@ public function processNode(Node $node, Scope $scope): array $extendsTags = $trait->getRequireExtendsTags(); foreach ($extendsTags as $extendsTag) { $type = $extendsTag->getType(); - if (!$type instanceof ObjectType) { - continue; - } + foreach ($type->getObjectClassNames() as $className) { + if ($classReflection->is($className)) { + continue; + } - if ($classReflection->is($type->getClassName())) { - continue; - } + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Trait %s requires using class to extend %s, but %s does not.', + $trait->getDisplayName(), + $type->describe(VerbosityLevel::typeOnly()), + $classReflection->getDisplayName(), + ), + ) + ->identifier('class.missingExtends') + ->build(); - $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; + } } } diff --git a/src/Rules/Classes/RequireImplementsRule.php b/src/Rules/Classes/RequireImplementsRule.php index 32c9361d65..4e0f368308 100644 --- a/src/Rules/Classes/RequireImplementsRule.php +++ b/src/Rules/Classes/RequireImplementsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,7 +15,8 @@ /** * @implements Rule */ -class RequireImplementsRule implements Rule +#[RegisteredRule(level: 2)] +final class RequireImplementsRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Classes/TraitAttributeClassRule.php b/src/Rules/Classes/TraitAttributeClassRule.php index 4d69f3a7ba..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 diff --git a/src/Rules/Classes/UnusedConstructorParametersRule.php b/src/Rules/Classes/UnusedConstructorParametersRule.php index 2850e2bafe..ca734d1867 100644 --- a/src/Rules/Classes/UnusedConstructorParametersRule.php +++ b/src/Rules/Classes/UnusedConstructorParametersRule.php @@ -6,6 +6,7 @@ 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\Rules\Rule; @@ -15,14 +16,13 @@ use function array_map; use function array_values; use function count; -use function is_string; use function sprintf; -use function strtolower; /** * @implements Rule */ -class UnusedConstructorParametersRule implements Rule +#[RegisteredRule(level: 1)] +final class UnusedConstructorParametersRule implements Rule { public function __construct(private UnusedFunctionParametersCheck $check) @@ -38,13 +38,22 @@ public function processNode(Node $node, Scope $scope): array { $method = $node->getMethodReflection(); $originalNode = $node->getOriginalNode(); - if (strtolower($method->getName()) !== '__construct' || $originalNode->stmts === null) { + if (!$method->isConstructor() || $originalNode->stmts === null) { return []; } if (count($originalNode->params) === 0) { return []; } + if ($node->getClassReflection()->isAttributeClass()) { + return []; + } + + foreach ($node->getClassReflection()->getInterfaces() as $interface) { + if ($interface->hasConstructor()) { + return []; + } + } $message = sprintf( 'Constructor of class %s has an unused parameter $%%s.', @@ -56,11 +65,11 @@ public function processNode(Node $node, Scope $scope): array return $this->check->getUnusedParameters( $scope, - array_map(static function (Param $parameter): string { - if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { + array_map(static function (Param $parameter): Variable { + if (!$parameter->var instanceof Variable) { throw new ShouldNotHappenException(); } - return $parameter->var->name; + return $parameter->var; }, array_values(array_filter($originalNode->params, static fn (Param $parameter): bool => $parameter->flags === 0))), $originalNode->stmts, $message, diff --git a/src/Rules/Comparison/BooleanAndConstantConditionRule.php b/src/Rules/Comparison/BooleanAndConstantConditionRule.php index 44c574fb68..d9945748b3 100644 --- a/src/Rules/Comparison/BooleanAndConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanAndConstantConditionRule.php @@ -4,6 +4,8 @@ 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; @@ -15,14 +17,18 @@ /** * @implements Rule */ -class BooleanAndConstantConditionRule implements Rule +#[RegisteredRule(level: 4)] +final class BooleanAndConstantConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, - private bool $bleedingEdge, + #[AutowiredParameter] private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -39,7 +45,7 @@ public function processNode( { $errors = []; $originalNode = $node->getOriginalNode(); - $nodeText = $this->bleedingEdge ? $originalNode->getOperatorSigil() : '&&'; + $nodeText = $originalNode->getOperatorSigil(); $leftType = $this->helper->getBooleanType($scope, $originalNode->left); $identifierType = $originalNode instanceof Node\Expr\BinaryOp\BooleanAnd ? 'booleanAnd' : 'logicalAnd'; if ($leftType instanceof ConstantBooleanType) { @@ -52,6 +58,9 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; @@ -90,6 +99,9 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; @@ -122,6 +134,9 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; diff --git a/src/Rules/Comparison/BooleanNotConstantConditionRule.php b/src/Rules/Comparison/BooleanNotConstantConditionRule.php index d41cb743ca..3a462cc836 100644 --- a/src/Rules/Comparison/BooleanNotConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanNotConstantConditionRule.php @@ -4,6 +4,8 @@ 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; @@ -13,13 +15,18 @@ /** * @implements Rule */ -class BooleanNotConstantConditionRule implements Rule +#[RegisteredRule(level: 4)] +final class BooleanNotConstantConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -45,6 +52,9 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; diff --git a/src/Rules/Comparison/BooleanOrConstantConditionRule.php b/src/Rules/Comparison/BooleanOrConstantConditionRule.php index 831829355c..199410f197 100644 --- a/src/Rules/Comparison/BooleanOrConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanOrConstantConditionRule.php @@ -4,6 +4,8 @@ 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; @@ -15,14 +17,18 @@ /** * @implements Rule */ -class BooleanOrConstantConditionRule implements Rule +#[RegisteredRule(level: 4)] +final class BooleanOrConstantConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, - private bool $bleedingEdge, + #[AutowiredParameter] private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -38,7 +44,7 @@ public function processNode( ): array { $originalNode = $node->getOriginalNode(); - $nodeText = $this->bleedingEdge ? $originalNode->getOperatorSigil() : '||'; + $nodeText = $originalNode->getOperatorSigil(); $messages = []; $leftType = $this->helper->getBooleanType($scope, $originalNode->left); $identifierType = $originalNode instanceof Node\Expr\BinaryOp\BooleanOr ? 'booleanOr' : 'logicalOr'; @@ -52,6 +58,9 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; @@ -90,6 +99,9 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; @@ -122,6 +134,9 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; diff --git a/src/Rules/Comparison/ConstantConditionRuleHelper.php b/src/Rules/Comparison/ConstantConditionRuleHelper.php index 60da2b56ff..7fc6692103 100644 --- a/src/Rules/Comparison/ConstantConditionRuleHelper.php +++ b/src/Rules/Comparison/ConstantConditionRuleHelper.php @@ -6,36 +6,27 @@ 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 { public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, - private bool $looseComparisonRuleEnabled, ) { } - public function shouldReportAlwaysTrueByDefault(Expr $expr): bool - { - return $expr instanceof Expr\BooleanNot - || $expr instanceof Expr\BinaryOp\BooleanOr - || $expr instanceof Expr\BinaryOp\BooleanAnd - || $expr instanceof Expr\Ternary - || $expr instanceof Expr\Isset_ - || $expr instanceof Expr\Empty_; - } - public function shouldSkip(Scope $scope, Expr $expr): bool { if ( - $this->looseComparisonRuleEnabled - && ($expr instanceof Expr\BinaryOp\Equal - || $expr instanceof Expr\BinaryOp\NotEqual - ) + $expr instanceof Expr\BinaryOp\Equal + || $expr instanceof Expr\BinaryOp\NotEqual ) { return true; } diff --git a/src/Rules/Comparison/ConstantLooseComparisonRule.php b/src/Rules/Comparison/ConstantLooseComparisonRule.php index 5222647d17..cc9a2ebdd7 100644 --- a/src/Rules/Comparison/ConstantLooseComparisonRule.php +++ b/src/Rules/Comparison/ConstantLooseComparisonRule.php @@ -4,23 +4,28 @@ 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 PHPStan\Type\VerbosityLevel; use function sprintf; /** * @implements Rule */ -class ConstantLooseComparisonRule implements Rule +#[RegisteredRule(level: 4)] +final class ConstantLooseComparisonRule implements Rule { public function __construct( - private bool $checkAlwaysTrueLooseComparison, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -37,7 +42,7 @@ public function processNode(Node $node, Scope $scope): array } $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); - if (!$nodeType instanceof ConstantBooleanType) { + if (!$nodeType->isTrue()->yes() && !$nodeType->isFalse()->yes()) { return []; } @@ -47,14 +52,17 @@ public function processNode(Node $node, Scope $scope): array } $instanceofTypeWithoutPhpDocs = $scope->getNativeType($node); - if ($instanceofTypeWithoutPhpDocs instanceof ConstantBooleanType) { + if ($instanceofTypeWithoutPhpDocs->isTrue()->yes() || $instanceofTypeWithoutPhpDocs->isFalse()->yes()) { + return $ruleErrorBuilder; + } + if (!$this->treatPhpDocTypesAsCertainTip) { return $ruleErrorBuilder; } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - if (!$nodeType->getValue()) { + if ($nodeType->isFalse()->yes()) { return [ $addTip(RuleErrorBuilder::message(sprintf( 'Loose comparison using %s between %s and %s will always evaluate to false.', @@ -63,28 +71,26 @@ public function processNode(Node $node, Scope $scope): array $scope->getType($node->right)->describe(VerbosityLevel::value()), )))->identifier(sprintf('%s.alwaysFalse', $node instanceof Node\Expr\BinaryOp\Equal ? 'equal' : 'notEqual'))->build(), ]; - } elseif ($this->checkAlwaysTrueLooseComparison) { - $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')); + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } - return [$errorBuilder->build()]; + $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.'); } - return []; + $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 6074f8d20c..5c5e193222 100644 --- a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php +++ b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php @@ -3,10 +3,12 @@ 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; @@ -16,12 +18,16 @@ /** * @implements Rule */ -class DoWhileLoopConstantConditionRule implements Rule +#[RegisteredRule(level: 4)] +final class DoWhileLoopConstantConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -47,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; @@ -70,6 +76,9 @@ public function processNode(Node $node, Scope $scope): array if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; diff --git a/src/Rules/Comparison/ElseIfConstantConditionRule.php b/src/Rules/Comparison/ElseIfConstantConditionRule.php index d1bf93a6db..bcf9ba5dde 100644 --- a/src/Rules/Comparison/ElseIfConstantConditionRule.php +++ b/src/Rules/Comparison/ElseIfConstantConditionRule.php @@ -4,6 +4,8 @@ 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; @@ -13,13 +15,18 @@ /** * @implements Rule */ -class ElseIfConstantConditionRule implements Rule +#[RegisteredRule(level: 4)] +final class ElseIfConstantConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -45,6 +52,9 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; diff --git a/src/Rules/Comparison/IfConstantConditionRule.php b/src/Rules/Comparison/IfConstantConditionRule.php index 8bf57ebcd4..a4b08ef043 100644 --- a/src/Rules/Comparison/IfConstantConditionRule.php +++ b/src/Rules/Comparison/IfConstantConditionRule.php @@ -4,6 +4,8 @@ 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; @@ -12,12 +14,16 @@ /** * @implements Rule */ -class IfConstantConditionRule implements Rule +#[RegisteredRule(level: 4)] +final class IfConstantConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -43,6 +49,9 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; diff --git a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php index 55423dbf50..9abb0b528c 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php @@ -4,23 +4,28 @@ 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; -use function strtolower; /** * @implements Rule */ -class ImpossibleCheckTypeFunctionCallRule implements Rule +#[RegisteredRule(level: 4)] +final class ImpossibleCheckTypeFunctionCallRule implements Rule { public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, - private bool $checkAlwaysTrueCheckTypeFunctionCall, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -37,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 []; @@ -54,6 +56,9 @@ public function processNode(Node $node, Scope $scope): array if ($isAlways !== null) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; @@ -66,27 +71,25 @@ public function processNode(Node $node, Scope $scope): array $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), )))->identifier('function.impossibleType')->build(), ]; - } elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) { - $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'); + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } - return [$errorBuilder->build()]; + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Call to function %s()%s will always evaluate to true.', + $functionName, + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + ))); + if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); } - return []; + $errorBuilder->identifier('function.alreadyNarrowedType'); + + return [$errorBuilder->build()]; } } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 828cfa1b82..0551a2b2d1 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -8,11 +8,15 @@ 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; @@ -37,7 +41,8 @@ use function sprintf; use function strtolower; -class ImpossibleCheckTypeHelper +#[AutowiredService] +final class ImpossibleCheckTypeHelper { /** @@ -46,9 +51,10 @@ class ImpossibleCheckTypeHelper public function __construct( private ReflectionProvider $reflectionProvider, private TypeSpecifier $typeSpecifier, + #[AutowiredParameter] private array $universalObjectCratesClasses, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, - private bool $nullContextForVoidReturningFunctions, ) { } @@ -68,11 +74,12 @@ public function findSpecifiedType( if ($functionName === 'assert' && $argsCount >= 1) { $arg = $node->getArgs()[0]->value; $assertValue = ($this->treatPhpDocTypesAsCertain ? $scope->getType($arg) : $scope->getNativeType($arg))->toBoolean(); - if (!$assertValue instanceof ConstantBooleanType) { + $assertValueIsTrue = $assertValue->isTrue()->yes(); + if (! $assertValueIsTrue && ! $assertValue->isFalse()->yes()) { return null; } - return $assertValue->getValue(); + return $assertValueIsTrue; } if (in_array($functionName, [ 'class_exists', @@ -146,7 +153,12 @@ public function findSpecifiedType( foreach ($haystackArrayTypes as $haystackArrayType) { if ($haystackArrayType instanceof ConstantArrayType) { foreach ($haystackArrayType->getValueTypes() as $i => $haystackArrayValueType) { - if ($haystackArrayType->isOptionalKey($i)) { + if (count($haystackArrayValueType->getFiniteTypes()) > 1 || $haystackArrayType->isOptionalKey($i)) { + continue; + } + + $haystackArrayValueConstantScalarTypes = $haystackArrayValueType->getConstantScalarTypes(); + if (count($haystackArrayValueConstantScalarTypes) > 1) { continue; } @@ -235,6 +247,10 @@ public function findSpecifiedType( } } + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + $typeSpecifierScope = $this->treatPhpDocTypesAsCertain ? $scope : $scope->doNotTreatPhpDocTypesAsCertain(); $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($typeSpecifierScope, $node, $this->determineContext($typeSpecifierScope, $node)); @@ -277,7 +293,7 @@ public function findSpecifiedType( /** @var Type $resultType */ $resultType = $sureType[1]; - $results[] = $resultType->isSuperTypeOf($argumentType); + $results[] = $resultType->isSuperTypeOf($argumentType)->result; } foreach ($sureNotTypes as $sureNotType) { @@ -295,7 +311,7 @@ public function findSpecifiedType( /** @var Type $resultType */ $resultType = $sureNotType[1]; - $results[] = $resultType->isSuperTypeOf($argumentType)->negate(); + $results[] = $resultType->isSuperTypeOf($argumentType)->negate()->result; } if (count($results) === 0) { @@ -369,16 +385,11 @@ public function doNotTreatPhpDocTypesAsCertain(): self $this->typeSpecifier, $this->universalObjectCratesClasses, false, - $this->nullContextForVoidReturningFunctions, ); } private function determineContext(Scope $scope, Expr $node): TypeSpecifierContext { - if (!$this->nullContextForVoidReturningFunctions) { - return TypeSpecifierContext::createTruthy(); - } - if ($node instanceof Expr\CallLike && $node->isFirstClassCallable()) { return TypeSpecifierContext::createTruthy(); } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php index 9fc4e4067d..9935a164f3 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php @@ -5,6 +5,8 @@ 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; @@ -15,14 +17,18 @@ /** * @implements Rule */ -class ImpossibleCheckTypeMethodCallRule implements Rule +#[RegisteredRule(level: 4)] +final class ImpossibleCheckTypeMethodCallRule implements Rule { public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, - private bool $checkAlwaysTrueCheckTypeFunctionCall, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -52,6 +58,9 @@ public function processNode(Node $node, Scope $scope): array if ($isAlways !== null) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; @@ -66,29 +75,27 @@ public function processNode(Node $node, Scope $scope): array $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), )))->identifier('method.impossibleType')->build(), ]; - } elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) { - $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'); + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } - return [$errorBuilder->build()]; + $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.'); } - return []; + $errorBuilder->identifier('method.alreadyNarrowedType'); + + return [$errorBuilder->build()]; } private function getMethod( diff --git a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php index 5d35d1be1a..3919824104 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php @@ -5,6 +5,8 @@ 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; @@ -15,14 +17,18 @@ /** * @implements Rule */ -class ImpossibleCheckTypeStaticMethodCallRule implements Rule +#[RegisteredRule(level: 4)] +final class ImpossibleCheckTypeStaticMethodCallRule implements Rule { public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, - private bool $checkAlwaysTrueCheckTypeFunctionCall, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -52,6 +58,9 @@ public function processNode(Node $node, Scope $scope): array if ($isAlways !== null) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; @@ -67,29 +76,27 @@ public function processNode(Node $node, Scope $scope): array $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), )))->identifier('staticMethod.impossibleType')->build(), ]; - } elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) { - $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.'); - } + } - $errorBuilder->identifier('staticMethod.alreadyNarrowedType'); + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } - return [$errorBuilder->build()]; + $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()]; } /** diff --git a/src/Rules/Comparison/LogicalXorConstantConditionRule.php b/src/Rules/Comparison/LogicalXorConstantConditionRule.php index 250b55bd6e..751280bf87 100644 --- a/src/Rules/Comparison/LogicalXorConstantConditionRule.php +++ b/src/Rules/Comparison/LogicalXorConstantConditionRule.php @@ -5,6 +5,8 @@ use PhpParser\Node; use PhpParser\Node\Expr\BinaryOp\LogicalXor; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,13 +16,18 @@ /** * @implements Rule */ -class LogicalXorConstantConditionRule implements Rule +#[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, ) { } @@ -44,6 +51,9 @@ public function processNode(Node $node, Scope $scope): array if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; @@ -77,6 +87,9 @@ public function processNode(Node $node, Scope $scope): array if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; diff --git a/src/Rules/Comparison/MatchExpressionRule.php b/src/Rules/Comparison/MatchExpressionRule.php index d7d318322e..cbfd3a5ea7 100644 --- a/src/Rules/Comparison/MatchExpressionRule.php +++ b/src/Rules/Comparison/MatchExpressionRule.php @@ -4,6 +4,8 @@ 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; @@ -22,14 +24,15 @@ /** * @implements Rule */ -class MatchExpressionRule implements Rule +#[RegisteredRule(level: 4)] +final class MatchExpressionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $constantConditionRuleHelper, - private bool $checkAlwaysTrueStrictComparison, - private bool $disableUnreachable, + #[AutowiredParameter] private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, ) { @@ -54,12 +57,6 @@ public function processNode(Node $node, Scope $scope): array $nextArmIsDeadForNativeType || ($nextArmIsDeadForType && $this->treatPhpDocTypesAsCertain) ) { - if (!$this->disableUnreachable) { - $errors[] = RuleErrorBuilder::message('Match arm is unreachable because previous comparison is always true.') - ->identifier('match.unreachable') - ->line($arm->getLine()) - ->build(); - } continue; } $armConditions = $arm->getConditions(); @@ -105,25 +102,24 @@ public function processNode(Node $node, Scope $scope): array $armConditionScope->getType($matchCondition)->describe(VerbosityLevel::value()), $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()), ))->line($armLine)->identifier('match.alwaysFalse')->build(); - } else { - if ($this->checkAlwaysTrueStrictComparison) { - if ($i === $armsCount - 1 && !$this->reportAlwaysTrueInLastCondition) { - continue; - } - $errorBuilder = RuleErrorBuilder::message(sprintf( - 'Match arm comparison between %s and %s is always true.', - $armConditionScope->getType($matchCondition)->describe(VerbosityLevel::value()), - $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()), - ))->line($armLine); - if ($i !== $armsCount - 1 && !$this->reportAlwaysTrueInLastCondition) { - $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); - } - - $errorBuilder->identifier('match.alwaysTrue'); - - $errors[] = $errorBuilder->build(); - } + continue; } + + if ($i === $armsCount - 1 && !$this->reportAlwaysTrueInLastCondition) { + continue; + } + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'Match arm comparison between %s and %s is always true.', + $armConditionScope->getType($matchCondition)->describe(VerbosityLevel::value()), + $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()), + ))->line($armLine); + if ($i !== $armsCount - 1 && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier('match.alwaysTrue'); + + $errors[] = $errorBuilder->build(); } } diff --git a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php index 081cdef1e0..14bf2c5a6d 100644 --- a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php +++ b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php @@ -5,6 +5,8 @@ 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; @@ -16,11 +18,15 @@ /** * @implements Rule */ -class NumberComparisonOperatorsConstantConditionRule implements Rule +#[RegisteredRule(level: 4)] +final class NumberComparisonOperatorsConstantConditionRule implements Rule { public function __construct( + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -55,6 +61,9 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; diff --git a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php index 66f447db3b..f0e71a0ee9 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -3,24 +3,36 @@ 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 Rule */ -class StrictComparisonOfDifferentTypesRule implements Rule +#[RegisteredRule(level: 4)] +final class StrictComparisonOfDifferentTypesRule implements Rule { public function __construct( - private bool $checkAlwaysTrueStrictComparison, + private RicherScopeGetTypeHelper $richerScopeGetTypeHelper, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] private bool $reportAlwaysTrueInLastCondition, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -32,11 +44,19 @@ 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 = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); + $nodeType = $nodeTypeResult->type; if (!$nodeType instanceof ConstantBooleanType) { return []; } @@ -44,7 +64,12 @@ public function processNode(Node $node, Scope $scope): array $leftType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->left) : $scope->getNativeType($node->left); $rightType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->right) : $scope->getNativeType($node->right); - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + $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; } @@ -53,51 +78,78 @@ public function processNode(Node $node, Scope $scope): array 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 [ $addTip(RuleErrorBuilder::message(sprintf( 'Strict comparison using %s between %s and %s will always evaluate to false.', $node->getOperatorSigil(), - $leftType->describe(VerbosityLevel::value()), - $rightType->describe(VerbosityLevel::value()), + $leftType->describe($verbosity), + $rightType->describe($verbosity), )))->identifier(sprintf('%s.alwaysFalse', $node instanceof Node\Expr\BinaryOp\Identical ? 'identical' : 'notIdentical'))->build(), ]; - } elseif ($this->checkAlwaysTrueStrictComparison) { - $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(VerbosityLevel::value()), - $rightType->describe(VerbosityLevel::value()), - ))); - 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.'); - } + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } - $errorBuilder->identifier(sprintf('%s.alwaysTrue', $node instanceof Node\Expr\BinaryOp\Identical ? 'identical' : 'notIdentical')); + $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.'); + } - return [ - $errorBuilder->build(), - ]; + if ( + $leftType->isEnum()->yes() + && $rightType->isEnum()->yes() + && $node->getAttribute(LastConditionVisitor::ATTRIBUTE_IS_MATCH_NAME, false) !== true + ) { + $errorBuilder->addTip('Use match expression instead. PHPStan will report unhandled enum cases.'); } - return []; + $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 9513924e6b..b6c515a4f3 100644 --- a/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php +++ b/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php @@ -4,6 +4,8 @@ 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; @@ -12,12 +14,16 @@ /** * @implements Rule */ -class TernaryOperatorConstantConditionRule implements Rule +#[RegisteredRule(level: 4)] +final class TernaryOperatorConstantConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -43,6 +49,9 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; diff --git a/src/Rules/Comparison/UnreachableIfBranchesRule.php b/src/Rules/Comparison/UnreachableIfBranchesRule.php deleted file mode 100644 index 4918a320d6..0000000000 --- a/src/Rules/Comparison/UnreachableIfBranchesRule.php +++ /dev/null @@ -1,80 +0,0 @@ - - */ -class UnreachableIfBranchesRule implements Rule -{ - - public function __construct( - private ConstantConditionRuleHelper $helper, - private bool $treatPhpDocTypesAsCertain, - private bool $disable, - ) - { - } - - public function getNodeType(): string - { - return Node\Stmt\If_::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if ($this->disable) { - return []; - } - - $errors = []; - $condition = $node->cond; - $conditionType = $this->treatPhpDocTypesAsCertain ? $scope->getType($condition) : $scope->getNativeType($condition); - $conditionBooleanType = $conditionType->toBoolean(); - $nextBranchIsDead = $conditionBooleanType->isTrue()->yes() && $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->getNativeType($condition)->toBoolean(); - if ($booleanNativeType instanceof ConstantBooleanType) { - return $ruleErrorBuilder; - } - - return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); - }; - - foreach ($node->elseifs as $elseif) { - if ($nextBranchIsDead) { - $errors[] = $addTip(RuleErrorBuilder::message('Elseif branch is unreachable because previous condition is always true.')) - ->identifier('elseif.unreachable') - ->line($elseif->getStartLine()) - ->build(); - continue; - } - - $condition = $elseif->cond; - $conditionType = $this->treatPhpDocTypesAsCertain ? $scope->getType($condition) : $scope->getNativeType($condition); - $conditionBooleanType = $conditionType->toBoolean(); - $nextBranchIsDead = $conditionBooleanType->isTrue()->yes() && $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.')) - ->identifier('else.unreachable') - ->line($node->else->getStartLine()) - ->build(); - } - - return $errors; - } - -} diff --git a/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php b/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php deleted file mode 100644 index 32ef874c5f..0000000000 --- a/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php +++ /dev/null @@ -1,66 +0,0 @@ - - */ -class UnreachableTernaryElseBranchRule implements Rule -{ - - public function __construct( - private ConstantConditionRuleHelper $helper, - private bool $treatPhpDocTypesAsCertain, - private bool $disable, - ) - { - } - - public function getNodeType(): string - { - return Node\Expr\Ternary::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if ($this->disable) { - return []; - } - - $conditionType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->cond) : $scope->getNativeType($node->cond); - $conditionBooleanType = $conditionType->toBoolean(); - if ( - $conditionBooleanType->isTrue()->yes() - && $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->getNativeType($node->cond); - if ($booleanNativeType instanceof ConstantBooleanType) { - return $ruleErrorBuilder; - } - - return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); - }; - return [ - $addTip(RuleErrorBuilder::message('Else branch is unreachable because ternary operator condition is always true.')) - ->identifier('ternary.elseUnreachable') - ->line($node->else->getStartLine()) - ->build(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php b/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php index 80a473b713..6016e72515 100644 --- a/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php +++ b/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.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 UsageOfVoidMatchExpressionRule implements Rule +#[RegisteredRule(level: 2)] +final class UsageOfVoidMatchExpressionRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php index 01ae378030..1057d4d3d7 100644 --- a/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php +++ b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php @@ -5,6 +5,8 @@ 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; @@ -12,12 +14,16 @@ /** * @implements Rule */ -class WhileLoopAlwaysFalseConditionRule implements Rule +#[RegisteredRule(level: 4)] +final class WhileLoopAlwaysFalseConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -43,6 +49,9 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; diff --git a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php index 42a885b6ad..86d0f01f2c 100644 --- a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php +++ b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php @@ -3,10 +3,12 @@ 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\BreaklessWhileLoopNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -15,12 +17,16 @@ /** * @implements Rule */ -class WhileLoopAlwaysTrueConditionRule implements Rule +#[RegisteredRule(level: 4)] +final class WhileLoopAlwaysTrueConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -46,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; @@ -70,6 +76,9 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; diff --git a/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php b/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php index 4e3bdcc92d..977bac234b 100644 --- a/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php +++ b/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php @@ -2,7 +2,7 @@ namespace PHPStan\Rules\Constants; -use PHPStan\Reflection\ConstantReflection; +use PHPStan\Reflection\ClassConstantReflection; /** * This is the extension interface to implement if you want to describe @@ -25,6 +25,6 @@ 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 27003ecade..bfb35fa6f4 100644 --- a/src/Rules/Constants/ConstantRule.php +++ b/src/Rules/Constants/ConstantRule.php @@ -4,6 +4,8 @@ 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; @@ -11,9 +13,17 @@ /** * @implements Rule */ -class ConstantRule implements 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; @@ -22,14 +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, - )) - ->identifier('constant.notFound') - ->discoveringSymbolsTip() - ->build(), + $errorBuilder->build(), ]; } diff --git a/src/Rules/Constants/DynamicClassConstantFetchRule.php b/src/Rules/Constants/DynamicClassConstantFetchRule.php index ce1295ffc4..62046c1b50 100644 --- a/src/Rules/Constants/DynamicClassConstantFetchRule.php +++ b/src/Rules/Constants/DynamicClassConstantFetchRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\ClassConstFetch; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -17,7 +18,8 @@ /** * @implements Rule */ -class DynamicClassConstantFetchRule implements Rule +#[RegisteredRule(level: 0)] +final class DynamicClassConstantFetchRule implements Rule { public function __construct(private PhpVersion $phpVersion, private RuleLevelHelper $ruleLevelHelper) diff --git a/src/Rules/Constants/FinalConstantRule.php b/src/Rules/Constants/FinalConstantRule.php index d6b891f465..6a72e67e1f 100644 --- a/src/Rules/Constants/FinalConstantRule.php +++ b/src/Rules/Constants/FinalConstantRule.php @@ -5,12 +5,14 @@ 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 { public function __construct(private PhpVersion $phpVersion) 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 895fb96360..cc325cdc33 100644 --- a/src/Rules/Constants/LazyAlwaysUsedClassConstantsExtensionProvider.php +++ b/src/Rules/Constants/LazyAlwaysUsedClassConstantsExtensionProvider.php @@ -2,9 +2,11 @@ namespace PHPStan\Rules\Constants; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; -class LazyAlwaysUsedClassConstantsExtensionProvider implements AlwaysUsedClassConstantsExtensionProvider +#[AutowiredService] +final class LazyAlwaysUsedClassConstantsExtensionProvider implements AlwaysUsedClassConstantsExtensionProvider { /** @var AlwaysUsedClassConstantsExtension[]|null */ @@ -16,11 +18,7 @@ public function __construct(private 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 index 5cfb38f074..0c5cb195dc 100644 --- a/src/Rules/Constants/MagicConstantContextRule.php +++ b/src/Rules/Constants/MagicConstantContextRule.php @@ -5,13 +5,15 @@ use PhpParser\Node; use PhpParser\Node\Scalar\MagicConst; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Parser\MagicConstantParamDefaultVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; /** @implements Rule */ -class MagicConstantContextRule implements Rule +#[RegisteredRule(level: 0)] +final class MagicConstantContextRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Constants/MissingClassConstantTypehintRule.php b/src/Rules/Constants/MissingClassConstantTypehintRule.php index 8a9aee1355..bb2d10164b 100644 --- a/src/Rules/Constants/MissingClassConstantTypehintRule.php +++ b/src/Rules/Constants/MissingClassConstantTypehintRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\MissingTypehintCheck; @@ -12,12 +13,12 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\VerbosityLevel; use function array_merge; -use function implode; use function sprintf; /** * @implements Rule */ +#[RegisteredRule(level: 6)] final class MissingClassConstantTypehintRule implements Rule { @@ -76,7 +77,7 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Constants/NativeTypedClassConstantRule.php b/src/Rules/Constants/NativeTypedClassConstantRule.php index ce2ea802d8..ab00605542 100644 --- a/src/Rules/Constants/NativeTypedClassConstantRule.php +++ b/src/Rules/Constants/NativeTypedClassConstantRule.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,7 +12,8 @@ /** * @implements Rule */ -class NativeTypedClassConstantRule implements Rule +#[RegisteredRule(level: 0)] +final class NativeTypedClassConstantRule implements Rule { public function __construct(private PhpVersion $phpVersion) diff --git a/src/Rules/Constants/OverridingConstantRule.php b/src/Rules/Constants/OverridingConstantRule.php index 555cbfc0dd..78cb6faff7 100644 --- a/src/Rules/Constants/OverridingConstantRule.php +++ b/src/Rules/Constants/OverridingConstantRule.php @@ -4,9 +4,10 @@ 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\RuleErrorBuilder; @@ -18,10 +19,12 @@ /** * @implements Rule */ -class OverridingConstantRule implements Rule +#[RegisteredRule(level: 0)] +final class OverridingConstantRule implements Rule { public function __construct( + #[AutowiredParameter] private bool $checkPhpDocMethodSignatures, ) { @@ -53,7 +56,7 @@ public function processNode(Node $node, Scope $scope): array private function processSingleConstant(ClassReflection $classReflection, string $constantName): array { $prototype = $this->findPrototype($classReflection, $constantName); - if (!$prototype instanceof ClassConstantReflection) { + if ($prototype === null) { return []; } @@ -145,7 +148,7 @@ private function processSingleConstant(ClassReflection $classReflection, string 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 index 37b72e1c72..9041a60ef0 100644 --- a/src/Rules/Constants/ValueAssignedToClassConstantRule.php +++ b/src/Rules/Constants/ValueAssignedToClassConstantRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; @@ -18,7 +19,8 @@ /** * @implements Rule */ -class ValueAssignedToClassConstantRule implements Rule +#[RegisteredRule(level: 2)] +final class ValueAssignedToClassConstantRule implements Rule { public function getNodeType(): string @@ -63,7 +65,7 @@ private function processSingleConstant(ClassReflection $classReflection, string return []; } - $accepts = $nativeType->acceptsWithReason($valueExprType, true); + $accepts = $nativeType->accepts($valueExprType, true); if ($accepts->yes()) { return []; } @@ -107,7 +109,7 @@ private function processSingleConstant(ClassReflection $classReflection, string } $type = $constantReflection->getValueType(); - $accepts = $type->acceptsWithReason($valueExprType, true); + $accepts = $type->accepts($valueExprType, true); if ($accepts->yes()) { return []; } diff --git a/src/Rules/DateTimeInstantiationRule.php b/src/Rules/DateTimeInstantiationRule.php index b8cd1684dd..360d6dccd2 100644 --- a/src/Rules/DateTimeInstantiationRule.php +++ b/src/Rules/DateTimeInstantiationRule.php @@ -6,6 +6,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\New_; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use Throwable; use function count; use function in_array; @@ -15,7 +16,8 @@ /** * @implements Rule */ -class DateTimeInstantiationRule implements Rule +#[RegisteredRule(level: 5)] +final class DateTimeInstantiationRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/DeadCode/BetterNoopRule.php b/src/Rules/DeadCode/BetterNoopRule.php deleted file mode 100644 index 22ce47032f..0000000000 --- a/src/Rules/DeadCode/BetterNoopRule.php +++ /dev/null @@ -1,138 +0,0 @@ - - */ -class BetterNoopRule implements Rule -{ - - public function __construct(private ExprPrinter $exprPrinter) - { - } - - public function getNodeType(): string - { - return NoopExpressionNode::class; - } - - public function processNode(Node $node, Scope $scope): array - { - $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\NullsafeMethodCall - || $expr instanceof Node\Expr\MethodCall - || $expr instanceof Node\Expr\StaticCall - ) { - // handled by *WithoutSideEffectsRule rules - return []; - } - - if ( - $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.', - $exprString, - ))->line($expr->getStartLine()) - ->identifier('expr.resultUnused') - ->build(), - ]; - } - -} diff --git a/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php index ab14e74d36..71673e8e43 100644 --- a/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php +++ b/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\CollectedDataNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,7 +15,8 @@ /** * @implements Rule */ -class CallToConstructorStatementWithoutImpurePointsRule implements Rule +#[RegisteredRule(level: 4)] +final class CallToConstructorStatementWithoutImpurePointsRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php index 1635b12050..06d710a83a 100644 --- a/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php +++ b/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\CollectedDataNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,7 +15,8 @@ /** * @implements Rule */ -class CallToFunctionStatementWithoutImpurePointsRule implements Rule +#[RegisteredRule(level: 4)] +final class CallToFunctionStatementWithoutImpurePointsRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php index 5c4d597cd9..e51c232112 100644 --- a/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php +++ b/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\CollectedDataNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,7 +15,8 @@ /** * @implements Rule */ -class CallToMethodStatementWithoutImpurePointsRule implements Rule +#[RegisteredRule(level: 4)] +final class CallToMethodStatementWithoutImpurePointsRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php index c64b29f8bf..8979b774b6 100644 --- a/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php +++ b/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\CollectedDataNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,7 +15,8 @@ /** * @implements Rule */ -class CallToStaticMethodStatementWithoutImpurePointsRule implements Rule +#[RegisteredRule(level: 4)] +final class CallToStaticMethodStatementWithoutImpurePointsRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php b/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php index dae162500a..86d93e1c4a 100644 --- a/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php +++ b/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php @@ -5,15 +5,15 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Collectors\Collector; +use PHPStan\DependencyInjection\RegisteredCollector; use PHPStan\Node\MethodReturnStatementsNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use function count; -use function strtolower; /** * @implements Collector */ -class ConstructorWithoutImpurePointsCollector implements Collector +#[RegisteredCollector(level: 4)] +final class ConstructorWithoutImpurePointsCollector implements Collector { public function getNodeType(): string @@ -24,7 +24,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope) { $method = $node->getMethodReflection(); - if (strtolower($method->getName()) !== '__construct') { + if (!$method->isConstructor()) { return null; } @@ -40,8 +40,7 @@ public function processNode(Node $node, Scope $scope) return null; } - $variant = ParametersAcceptorSelector::selectSingle($method->getVariants()); - foreach ($variant->getParameters() as $parameter) { + foreach ($method->getParameters() as $parameter) { if (!$parameter->passedByReference()->createsNewVariable()) { continue; } diff --git a/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php b/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php index 528e5c76e6..d7abebe3eb 100644 --- a/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php +++ b/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php @@ -5,14 +5,15 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Collectors\Collector; +use PHPStan\DependencyInjection\RegisteredCollector; use PHPStan\Node\FunctionReturnStatementsNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use function count; /** * @implements Collector */ -class FunctionWithoutImpurePointsCollector implements Collector +#[RegisteredCollector(level: 4)] +final class FunctionWithoutImpurePointsCollector implements Collector { public function getNodeType(): string @@ -38,8 +39,7 @@ public function processNode(Node $node, Scope $scope) return null; } - $variant = ParametersAcceptorSelector::selectSingle($function->getVariants()); - foreach ($variant->getParameters() as $parameter) { + foreach ($function->getParameters() as $parameter) { if (!$parameter->passedByReference()->createsNewVariable()) { continue; } diff --git a/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php b/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php index b6bdb3ca34..776e55969c 100644 --- a/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php +++ b/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php @@ -5,14 +5,15 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Collectors\Collector; +use PHPStan\DependencyInjection\RegisteredCollector; use PHPStan\Node\MethodReturnStatementsNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use function count; /** * @implements Collector */ -class MethodWithoutImpurePointsCollector implements Collector +#[RegisteredCollector(level: 4)] +final class MethodWithoutImpurePointsCollector implements Collector { public function getNodeType(): string @@ -38,8 +39,7 @@ public function processNode(Node $node, Scope $scope) return null; } - $variant = ParametersAcceptorSelector::selectSingle($method->getVariants()); - foreach ($variant->getParameters() as $parameter) { + foreach ($method->getParameters() as $parameter) { if (!$parameter->passedByReference()->createsNewVariable()) { continue; } @@ -51,11 +51,7 @@ public function processNode(Node $node, Scope $scope) return null; } - $declaringClass = $method->getDeclaringClass(); - if ( - $declaringClass->hasConstructor() - && $declaringClass->getConstructor()->getName() === $method->getName() - ) { + if ($method->isConstructor()) { return null; } diff --git a/src/Rules/DeadCode/NoopRule.php b/src/Rules/DeadCode/NoopRule.php index 34855261ef..4a8cb110bd 100644 --- a/src/Rules/DeadCode/NoopRule.php +++ b/src/Rules/DeadCode/NoopRule.php @@ -4,70 +4,137 @@ use PhpParser\Node; 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; /** - * @deprecated Replaced by PHPStan\Rules\DeadCode\BetterNoopRule - * @implements Rule + * @implements Rule */ -class NoopRule implements Rule +#[RegisteredRule(level: 4)] +final class NoopRule implements Rule { - public function __construct(private ExprPrinter $exprPrinter, private bool $better) + public function __construct(private ExprPrinter $exprPrinter) { } public function getNodeType(): string { - return Node\Stmt\Expression::class; + return NoopExpressionNode::class; } public function processNode(Node $node, Scope $scope): array { - if ($this->better) { - // disabled in bleeding edge + $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\NullsafeMethodCall + || $expr instanceof Node\Expr\MethodCall + || $expr instanceof Node\Expr\StaticCall + ) { + // handled by *WithoutSideEffectsRule rules return []; } - $originalExpr = $node->expr; - $expr = $originalExpr; + 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\Assign + || $expr instanceof Node\Expr\AssignOp + || $expr instanceof Node\Expr\AssignRef ) { - $expr = $expr->expr; + return []; } - if (!$this->isNoopExpr($expr)) { + 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->exprPrinter->printExpr($originalExpr), + $exprString, ))->line($expr->getStartLine()) ->identifier('expr.resultUnused') ->build(), ]; } - public function isNoopExpr(Node\Expr $expr): bool - { - return $expr instanceof Node\Expr\Variable - || $expr instanceof Node\Expr\PropertyFetch - || $expr instanceof Node\Expr\StaticPropertyFetch - || $expr instanceof Node\Expr\NullsafePropertyFetch - || $expr instanceof Node\Expr\ArrayDimFetch - || $expr instanceof Node\Scalar - || $expr instanceof Node\Expr\Isset_ - || $expr instanceof Node\Expr\Empty_ - || $expr instanceof Node\Expr\ConstFetch - || $expr instanceof Node\Expr\ClassConstFetch; - } - } diff --git a/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php b/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php index 952da73ba2..4147f0b0e0 100644 --- a/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php +++ b/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php @@ -6,12 +6,14 @@ use PhpParser\Node\Stmt\Expression; use PHPStan\Analyser\Scope; use PHPStan\Collectors\Collector; +use PHPStan\DependencyInjection\RegisteredCollector; use PHPStan\Reflection\ReflectionProvider; /** * @implements Collector */ -class PossiblyPureFuncCallCollector implements Collector +#[RegisteredCollector(level: 4)] +final class PossiblyPureFuncCallCollector implements Collector { public function __construct(private ReflectionProvider $reflectionProvider) diff --git a/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php b/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php index 256d2bf781..02c24768c9 100644 --- a/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php +++ b/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php @@ -6,11 +6,13 @@ use PhpParser\Node\Stmt\Expression; use PHPStan\Analyser\Scope; use PHPStan\Collectors\Collector; +use PHPStan\DependencyInjection\RegisteredCollector; /** * @implements Collector, string, int}> */ -class PossiblyPureMethodCallCollector implements Collector +#[RegisteredCollector(level: 4)] +final class PossiblyPureMethodCallCollector implements Collector { public function __construct() diff --git a/src/Rules/DeadCode/PossiblyPureNewCollector.php b/src/Rules/DeadCode/PossiblyPureNewCollector.php index e2fabe49ca..54d8e1da7a 100644 --- a/src/Rules/DeadCode/PossiblyPureNewCollector.php +++ b/src/Rules/DeadCode/PossiblyPureNewCollector.php @@ -6,13 +6,15 @@ use PhpParser\Node\Stmt\Expression; use PHPStan\Analyser\Scope; use PHPStan\Collectors\Collector; +use PHPStan\DependencyInjection\RegisteredCollector; use PHPStan\Reflection\ReflectionProvider; use function strtolower; /** * @implements Collector */ -class PossiblyPureNewCollector implements Collector +#[RegisteredCollector(level: 4)] +final class PossiblyPureNewCollector implements Collector { public function __construct(private ReflectionProvider $reflectionProvider) diff --git a/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php b/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php index d934b57a6d..b8204335c0 100644 --- a/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php +++ b/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php @@ -6,11 +6,13 @@ use PhpParser\Node\Stmt\Expression; use PHPStan\Analyser\Scope; use PHPStan\Collectors\Collector; +use PHPStan\DependencyInjection\RegisteredCollector; /** * @implements Collector */ -class PossiblyPureStaticCallCollector implements Collector +#[RegisteredCollector(level: 4)] +final class PossiblyPureStaticCallCollector implements Collector { public function __construct() diff --git a/src/Rules/DeadCode/UnreachableStatementRule.php b/src/Rules/DeadCode/UnreachableStatementRule.php index f1b226ad57..92618e4855 100644 --- a/src/Rules/DeadCode/UnreachableStatementRule.php +++ b/src/Rules/DeadCode/UnreachableStatementRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\UnreachableStatementNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -11,7 +12,8 @@ /** * @implements Rule */ -class UnreachableStatementRule implements Rule +#[RegisteredRule(level: 4)] +final class UnreachableStatementRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/DeadCode/UnusedPrivateConstantRule.php b/src/Rules/DeadCode/UnusedPrivateConstantRule.php index ccb160f60f..6e017873d2 100644 --- a/src/Rules/DeadCode/UnusedPrivateConstantRule.php +++ b/src/Rules/DeadCode/UnusedPrivateConstantRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClassConstantsNode; use PHPStan\Rules\Constants\AlwaysUsedClassConstantsExtensionProvider; use PHPStan\Rules\Rule; @@ -14,7 +15,8 @@ /** * @implements Rule */ -class UnusedPrivateConstantRule implements Rule +#[RegisteredRule(level: 4)] +final class UnusedPrivateConstantRule implements Rule { public function __construct(private AlwaysUsedClassConstantsExtensionProvider $extensionProvider) @@ -33,7 +35,7 @@ public function processNode(Node $node, Scope $scope): array } $classReflection = $node->getClassReflection(); - $classType = new ObjectType($classReflection->getName(), null, $classReflection); + $classType = new ObjectType($classReflection->getName(), classReflection: $classReflection); $constants = []; foreach ($node->getConstants() as $constant) { diff --git a/src/Rules/DeadCode/UnusedPrivateMethodRule.php b/src/Rules/DeadCode/UnusedPrivateMethodRule.php index aa868808d4..2c1ab6ce64 100644 --- a/src/Rules/DeadCode/UnusedPrivateMethodRule.php +++ b/src/Rules/DeadCode/UnusedPrivateMethodRule.php @@ -5,6 +5,7 @@ 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; @@ -20,7 +21,8 @@ /** * @implements Rule */ -class UnusedPrivateMethodRule implements Rule +#[RegisteredRule(level: 4)] +final class UnusedPrivateMethodRule implements Rule { public function __construct(private AlwaysUsedMethodExtensionProvider $extensionProvider) @@ -38,7 +40,7 @@ public function processNode(Node $node, Scope $scope): array return []; } $classReflection = $node->getClassReflection(); - $classType = new ObjectType($classReflection->getName(), null, $classReflection); + $classType = new ObjectType($classReflection->getName(), classReflection: $classReflection); $constructor = null; if ($classReflection->hasConstructor()) { $constructor = $classReflection->getConstructor(); @@ -84,7 +86,16 @@ public function processNode(Node $node, Scope $scope): array $methodNameType = $callScope->getType($methodCallNode->name); $strings = $methodNameType->getConstantStrings(); if (count($strings) === 0) { - return []; + // handle subtractions of a dynamic method call + foreach ($methods as $lowerMethodName => $method) { + if ((new ConstantStringType($method->getNode()->name->toString()))->isSuperTypeOf($methodNameType)->no()) { + continue; + } + + unset($methods[$lowerMethodName]); + } + + continue; } $methodNames = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $strings); diff --git a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php index 6af054659d..c6bb859d6d 100644 --- a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php +++ b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php @@ -4,8 +4,11 @@ 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; @@ -14,13 +17,16 @@ 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 { /** @@ -29,8 +35,11 @@ class UnusedPrivatePropertyRule implements Rule */ public function __construct( private ReadWritePropertiesExtensionProvider $extensionProvider, + #[AutowiredParameter(ref: '%propertyAlwaysWrittenTags%')] private array $alwaysWrittenTags, + #[AutowiredParameter(ref: '%propertyAlwaysReadTags%')] private array $alwaysReadTags, + #[AutowiredParameter] private bool $checkUninitializedProperties, ) { @@ -47,7 +56,7 @@ public function processNode(Node $node, Scope $scope): array return []; } $classReflection = $node->getClassReflection(); - $classType = new ObjectType($classReflection->getName(), null, $classReflection); + $classType = new ObjectType($classReflection->getName(), classReflection: $classReflection); $properties = []; foreach ($node->getProperties() as $property) { if (!$property->isPrivate()) { @@ -57,8 +66,8 @@ public function processNode(Node $node, Scope $scope): array continue; } - $alwaysRead = false; - $alwaysWritten = false; + $alwaysRead = !$property->isReadable(); + $alwaysWritten = !$property->isWritable(); if ($property->getPhpDoc() !== null) { $text = $property->getPhpDoc(); foreach ($this->alwaysReadTags as $tag) { @@ -109,29 +118,63 @@ 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); + $propertyNameType = $usageScope->getType($fetch->name); $strings = $propertyNameType->getConstantStrings(); if (count($strings) === 0) { - return []; + // handle subtractions of a dynamic property fetch + foreach ($properties as $propertyName => $data) { + if ((new ConstantStringType($propertyName))->isSuperTypeOf($propertyNameType)->no()) { + continue; + } + + unset($properties[$propertyName]); + } + + continue; } $propertyNames = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $strings); } + if ($fetch instanceof Node\Expr\PropertyFetch) { - $fetchedOnType = $usage->getScope()->getType($fetch->var); + $fetchedOnType = $usageScope->getType($fetch->var); } else { if ($fetch->class instanceof Node\Name) { - $fetchedOnType = $usage->getScope()->resolveTypeByName($fetch->class); + $fetchedOnType = $usageScope->resolveTypeByName($fetch->class); } else { - $fetchedOnType = $usage->getScope()->getType($fetch->class); + $fetchedOnType = $usageScope->getType($fetch->class); } } @@ -139,7 +182,7 @@ public function processNode(Node $node, Scope $scope): array if (!array_key_exists($propertyName, $properties)) { continue; } - $propertyReflection = $usage->getScope()->getPropertyReflection($fetchedOnType, $propertyName); + $propertyReflection = $usageScope->getPropertyReflection($fetchedOnType, $propertyName); if ($propertyReflection === null) { if (!$classType->isSuperTypeOf($fetchedOnType)->no()) { if ($usage instanceof PropertyRead) { @@ -188,18 +231,32 @@ public function processNode(Node $node, Scope $scope): array ->identifier('property.unused') ->build(); } else { - $errors[] = RuleErrorBuilder::message(sprintf('%s is never read, only written.', $propertyName)) + if ($data['onlyReadable']) { + $errors[] = RuleErrorBuilder::message(sprintf('Readable %s is never read.', lcfirst($propertyName))) + ->line($propertyNode->getStartLine()) + ->identifier('property.neverRead') + ->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf('%s is never read, only written.', $propertyName)) + ->line($propertyNode->getStartLine()) + ->identifier('property.onlyWritten') + ->tip($tip) + ->build(); + } + } + } elseif (!$data['written'] && (!array_key_exists($name, $uninitializedProperties) || !$this->checkUninitializedProperties)) { + if ($data['onlyWritable']) { + $errors[] = RuleErrorBuilder::message(sprintf('Writable %s is never written.', lcfirst($propertyName))) + ->line($propertyNode->getStartLine()) + ->identifier('property.neverWritten') + ->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf('%s is never written, only read.', $propertyName)) ->line($propertyNode->getStartLine()) - ->identifier('property.onlyWritten') + ->identifier('property.onlyRead') ->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()) - ->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 db1020b18a..5a628a95f5 100644 --- a/src/Rules/Debug/DumpTypeRule.php +++ b/src/Rules/Debug/DumpTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -15,7 +16,8 @@ /** * @implements Rule */ -class DumpTypeRule implements Rule +#[AutowiredService] +final class DumpTypeRule implements Rule { public function __construct(private ReflectionProvider $reflectionProvider) diff --git a/src/Rules/Debug/FileAssertRule.php b/src/Rules/Debug/FileAssertRule.php index e0899850c3..bf5af7456d 100644 --- a/src/Rules/Debug/FileAssertRule.php +++ b/src/Rules/Debug/FileAssertRule.php @@ -5,6 +5,8 @@ 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; @@ -18,10 +20,14 @@ /** * @implements Rule */ -class FileAssertRule implements Rule +#[AutowiredService] +final class FileAssertRule implements Rule { - public function __construct(private ReflectionProvider $reflectionProvider) + public function __construct( + private ReflectionProvider $reflectionProvider, + private TypeStringResolver $typeStringResolver, + ) { } @@ -49,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); } @@ -122,6 +132,40 @@ private function processAssertNativeType(array $args, Scope $scope): array ]; } + /** + * @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 subtype of %s, actual: %s', $expectedTypeStrings[0]->getValue(), $expressionType->describe(VerbosityLevel::precise()))) + ->nonIgnorable() + ->identifier('phpstan.superType') + ->build(), + ]; + } + /** * @param Node\Arg[] $args * @return list @@ -171,15 +215,14 @@ private function processAssertVariableCertainty(array $args, Scope $scope): arra // @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() - ->identifier('phpstan.unknownExpectation') - ->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() @@ -188,13 +231,12 @@ private function processAssertVariableCertainty(array $args, Scope $scope): arra ]; } - $actualCertaintyValue = $scope->hasVariableType($variable->name); if ($expectedCertaintyValue->equals($actualCertaintyValue)) { return []; } return [ - RuleErrorBuilder::message(sprintf('Expected variable certainty %s, actual: %s', $expectedCertaintyValue->describe(), $actualCertaintyValue->describe())) + 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 index 7ee53824ee..8003bef42a 100644 --- a/src/Rules/DirectRegistry.php +++ b/src/Rules/DirectRegistry.php @@ -6,7 +6,7 @@ use function class_implements; use function class_parents; -class DirectRegistry implements Registry +final class DirectRegistry implements Registry { /** @var Rule[][] */ @@ -27,10 +27,8 @@ public function __construct(array $rules) /** * @template TNodeType of Node - * @phpstan-param class-string $nodeType - * @param Node $nodeType - * @phpstan-return array> - * @return Rule[] + * @param class-string $nodeType + * @return array> */ public function getRules(string $nodeType): array { @@ -48,8 +46,7 @@ public function getRules(string $nodeType): array } /** - * @phpstan-var array> $selectedRules - * @var Rule[] $selectedRules + * @var array> $selectedRules */ $selectedRules = $this->cache[$nodeType]; diff --git a/src/Rules/EnumCases/EnumCaseAttributesRule.php b/src/Rules/EnumCases/EnumCaseAttributesRule.php index 8d584b72f6..f6489f2e87 100644 --- a/src/Rules/EnumCases/EnumCaseAttributesRule.php +++ b/src/Rules/EnumCases/EnumCaseAttributesRule.php @@ -5,13 +5,15 @@ use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** * @implements Rule */ -class EnumCaseAttributesRule implements Rule +#[RegisteredRule(level: 0)] +final class EnumCaseAttributesRule implements Rule { public function __construct(private AttributesCheck $attributesCheck) diff --git a/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php b/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php index 267d8c8200..f4e2479a2a 100644 --- a/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php +++ b/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php @@ -4,6 +4,8 @@ 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; @@ -14,11 +16,14 @@ /** * @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, ) { diff --git a/src/Rules/Exceptions/CaughtExceptionExistenceRule.php b/src/Rules/Exceptions/CaughtExceptionExistenceRule.php index 1231a0d5e2..3c99cca44a 100644 --- a/src/Rules/Exceptions/CaughtExceptionExistenceRule.php +++ b/src/Rules/Exceptions/CaughtExceptionExistenceRule.php @@ -5,9 +5,12 @@ 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\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use Throwable; @@ -17,13 +20,17 @@ /** * @implements Rule */ -class CaughtExceptionExistenceRule implements Rule +#[RegisteredRule(level: 0)] +final class CaughtExceptionExistenceRule implements Rule { public function __construct( private ReflectionProvider $reflectionProvider, private ClassNameCheck $classCheck, + #[AutowiredParameter] private bool $checkClassCaseSensitivity, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { } @@ -42,11 +49,16 @@ public function processNode(Node $node, Scope $scope): array if ($scope->isInClassExists($className)) { continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Caught class %s not found.', $className)) + + $errorBuilder = RuleErrorBuilder::message(sprintf('Caught class %s not found.', $className)) ->line($class->getStartLine()) - ->identifier('class.notFound') - ->discoveringSymbolsTip() - ->build(); + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); continue; } @@ -61,7 +73,9 @@ public function processNode(Node $node, Scope $scope): array $errors = array_merge( $errors, $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 cdb62d2597..70e2c5806e 100644 --- a/src/Rules/Exceptions/DefaultExceptionTypeResolver.php +++ b/src/Rules/Exceptions/DefaultExceptionTypeResolver.php @@ -4,11 +4,16 @@ use Nette\Utils\Strings; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ReflectionProvider; use function count; -/** @api */ -class DefaultExceptionTypeResolver implements ExceptionTypeResolver +/** + * @api + */ +#[AutowiredService(as: DefaultExceptionTypeResolver::class)] +final class DefaultExceptionTypeResolver implements ExceptionTypeResolver { /** @@ -19,9 +24,13 @@ class DefaultExceptionTypeResolver implements ExceptionTypeResolver */ public function __construct( 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, ) { @@ -47,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; } @@ -81,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/MissingCheckedExceptionInFunctionThrowsRule.php b/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php index e2ee44c466..f3ed6e08f8 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php @@ -12,7 +12,7 @@ /** * @implements Rule */ -class MissingCheckedExceptionInFunctionThrowsRule implements Rule +final class MissingCheckedExceptionInFunctionThrowsRule implements Rule { public function __construct(private MissingCheckedExceptionInThrowsCheck $check) diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php b/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php index 2aae5d488c..c564711b2f 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php @@ -12,7 +12,7 @@ /** * @implements Rule */ -class MissingCheckedExceptionInMethodThrowsRule implements Rule +final class MissingCheckedExceptionInMethodThrowsRule implements Rule { public function __construct(private MissingCheckedExceptionInThrowsCheck $check) 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 508214f275..62de7f9e2a 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php @@ -4,6 +4,8 @@ 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; @@ -12,10 +14,14 @@ use PHPStan\Type\VerbosityLevel; use Throwable; -class MissingCheckedExceptionInThrowsCheck +#[AutowiredService] +final class MissingCheckedExceptionInThrowsCheck { - public function __construct(private ExceptionTypeResolver $exceptionTypeResolver) + public function __construct( + #[AutowiredParameter(ref: '@exceptionTypeResolver')] + private ExceptionTypeResolver $exceptionTypeResolver, + ) { } diff --git a/src/Rules/Exceptions/NoncapturingCatchRule.php b/src/Rules/Exceptions/NoncapturingCatchRule.php index d91bf2fc14..664b1433d1 100644 --- a/src/Rules/Exceptions/NoncapturingCatchRule.php +++ b/src/Rules/Exceptions/NoncapturingCatchRule.php @@ -4,20 +4,17 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\Php\PhpVersion; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; /** * @implements Rule */ -class NoncapturingCatchRule implements Rule +#[RegisteredRule(level: 0)] +final class NoncapturingCatchRule implements Rule { - public function __construct(private PhpVersion $phpVersion) - { - } - public function getNodeType(): string { return Node\Stmt\Catch_::class; @@ -28,7 +25,7 @@ public function getNodeType(): string */ public function processNode(Node $node, Scope $scope): array { - if ($this->phpVersion->supportsNoncapturingCatches()) { + if ($scope->getPhpVersion()->supportsNoncapturingCatches()->yes()) { return []; } diff --git a/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php b/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php index bcf73954a9..aaaca0956b 100644 --- a/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php +++ b/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\FinallyExitPointsNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -13,7 +14,8 @@ /** * @implements Rule */ -class OverwrittenExitPointByFinallyRule implements Rule +#[RegisteredRule(level: 4)] +final class OverwrittenExitPointByFinallyRule implements Rule { public function getNodeType(): string @@ -51,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 index 85f648ab97..e2b0debb30 100644 --- a/src/Rules/Exceptions/ThrowExprTypeRule.php +++ b/src/Rules/Exceptions/ThrowExprTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -17,7 +18,8 @@ /** * @implements Rule */ -class ThrowExprTypeRule implements Rule +#[RegisteredRule(level: 3)] +final class ThrowExprTypeRule implements Rule { public function __construct( diff --git a/src/Rules/Exceptions/ThrowExpressionRule.php b/src/Rules/Exceptions/ThrowExpressionRule.php index d9b5655ff6..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,7 +13,8 @@ /** * @implements Rule */ -class ThrowExpressionRule implements Rule +#[RegisteredRule(level: 0)] +final class ThrowExpressionRule implements Rule { public function __construct(private PhpVersion $phpVersion) @@ -29,6 +32,10 @@ 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() ->identifier('throw.notSupported') diff --git a/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php b/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php index f07d6dc5c0..37dee9c0dd 100644 --- a/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php +++ b/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\FunctionReturnStatementsNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -15,11 +17,14 @@ /** * @implements Rule */ -class ThrowsVoidFunctionWithExplicitThrowPointRule implements Rule +#[RegisteredRule(level: 3)] +final class ThrowsVoidFunctionWithExplicitThrowPointRule implements Rule { public function __construct( + #[AutowiredParameter(ref: '@exceptionTypeResolver')] private ExceptionTypeResolver $exceptionTypeResolver, + #[AutowiredParameter(ref: '%exceptions.check.missingCheckedExceptionInThrows%')] private bool $missingCheckedExceptionInThrows, ) { diff --git a/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php b/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php index 59c0bd8448..4a97c6c4fc 100644 --- a/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php +++ b/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\MethodReturnStatementsNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -15,11 +17,14 @@ /** * @implements Rule */ -class ThrowsVoidMethodWithExplicitThrowPointRule implements Rule +#[RegisteredRule(level: 3)] +final class ThrowsVoidMethodWithExplicitThrowPointRule implements Rule { public function __construct( + #[AutowiredParameter(ref: '@exceptionTypeResolver')] private ExceptionTypeResolver $exceptionTypeResolver, + #[AutowiredParameter(ref: '%exceptions.check.missingCheckedExceptionInThrows%')] private bool $missingCheckedExceptionInThrows, ) { 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 e7f7f9849e..6688dec466 100644 --- a/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php +++ b/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php @@ -12,7 +12,7 @@ /** * @implements Rule */ -class TooWideFunctionThrowTypeRule implements Rule +final class TooWideFunctionThrowTypeRule implements Rule { public function __construct(private TooWideThrowTypeCheck $check) diff --git a/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php b/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php index c9a5f231b6..55c69ee3a5 100644 --- a/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php +++ b/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php @@ -13,7 +13,7 @@ /** * @implements Rule */ -class TooWideMethodThrowTypeRule implements Rule +final class TooWideMethodThrowTypeRule implements Rule { public function __construct(private FileTypeMapper $fileTypeMapper, private TooWideThrowTypeCheck $check) 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 2a15d3139f..1eb978a73f 100644 --- a/src/Rules/Exceptions/TooWideThrowTypeCheck.php +++ b/src/Rules/Exceptions/TooWideThrowTypeCheck.php @@ -3,6 +3,8 @@ 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; @@ -10,9 +12,17 @@ use PHPStan\Type\VerbosityLevel; use function array_map; -class TooWideThrowTypeCheck +#[AutowiredService] +final class TooWideThrowTypeCheck { + public function __construct( + #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] + private bool $implicitThrows, + ) + { + } + /** * @param ThrowPoint[] $throwPoints * @return string[] @@ -23,8 +33,8 @@ public function check(Type $throwType, array $throwPoints): array 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/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 @@ + */ @@ -61,9 +66,24 @@ public function check( ParametersAcceptor $parametersAcceptor, Scope $scope, bool $isBuiltin, - $funcCall, - array $messages, - string $nodeType = 'function', + 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,8 +107,13 @@ public function check( $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.') ->identifier('argument.unpackAfterNamed') @@ -97,47 +122,47 @@ public function check( ->build(); } if ($hasUnpackedArgument && !$arg->unpack) { - $errors[] = RuleErrorBuilder::message('Unpacked argument (...) cannot be followed by a non-unpacked argument.') - ->identifier('argument.nonUnpackAfterUnpacked') - ->line($arg->getStartLine()) - ->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) { + $type = $scope->getType($arg->value); $arrays = $type->getConstantArrays(); if (count($arrays) > 0) { - $minKeys = null; + $maxKeys = null; foreach ($arrays as $array) { $countType = $array->getArraySize(); if ($countType instanceof ConstantIntegerType) { $keysCount = $countType->getValue(); } elseif ($countType instanceof IntegerRangeType) { - $keysCount = $countType->getMin(); + $keysCount = $countType->getMax(); if ($keysCount === null) { throw new ShouldNotHappenException(); } } else { throw new ShouldNotHappenException(); } - if ($minKeys !== null && $keysCount >= $minKeys) { + 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) { @@ -151,6 +176,10 @@ public function check( $keyArgumentName = $commonKey; $hasNamedArguments = true; } + if ($isOptionalKey) { + continue; + } + $arguments[] = [ $arg->value, TypeCombinator::union(...$types), @@ -159,6 +188,16 @@ public function check( $arg->getStartLine(), ]; } + + if (count($arguments) === 0 && $type->isIterableAtLeastOnce()->yes()) { + $arguments[] = [ + $arg->value, + $type->getIterableValueType(), + true, + null, + $arg->getStartLine(), + ]; + } } else { $arguments[] = [ $arg->value, @@ -180,7 +219,7 @@ public function check( ]; } - if ($hasNamedArguments && !$this->phpVersion->supportsNamedArguments() && !(bool) $funcCall->getAttribute('isAttribute', false)) { + 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()) @@ -190,7 +229,7 @@ public function check( 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; @@ -203,7 +242,7 @@ public function check( ) { if ($functionParametersMinCount === $functionParametersMaxCount) { $errors[] = RuleErrorBuilder::message(sprintf( - $invokedParametersCount === 1 ? $messages[0] : $messages[1], + $invokedParametersCount === 1 ? $singleInsufficientParameterMessage : $pluralInsufficientParametersMessage, $invokedParametersCount, $functionParametersMinCount, )) @@ -212,7 +251,7 @@ public function check( ->build(); } elseif ($functionParametersMaxCount === -1 && $invokedParametersCount < $functionParametersMinCount) { $errors[] = RuleErrorBuilder::message(sprintf( - $invokedParametersCount === 1 ? $messages[2] : $messages[3], + $invokedParametersCount === 1 ? $singleInsufficientParameterInVariadicFunctionMessage : $pluralInsufficientParametersInVariadicFunctionMessage, $invokedParametersCount, $functionParametersMinCount, )) @@ -221,7 +260,7 @@ public function check( ->build(); } elseif ($functionParametersMaxCount !== -1) { $errors[] = RuleErrorBuilder::message(sprintf( - $invokedParametersCount === 1 ? $messages[4] : $messages[5], + $invokedParametersCount === 1 ? $singleInsufficientParameterWithOptionalParametersMessage : $pluralInsufficientParametersWithOptionalParametersMessage, $invokedParametersCount, $functionParametersMinCount, $functionParametersMaxCount, @@ -238,13 +277,13 @@ public function check( && !$scope->isInFirstLevelStatement() && $scope->getKeepVoidType($funcCall)->isVoid()->yes() ) { - $errors[] = RuleErrorBuilder::message($messages[7]) + $errors[] = RuleErrorBuilder::message($voidReturnTypeUsed) ->identifier(sprintf('%s.void', $nodeType)) ->line($funcCall->getStartLine()) ->build(); } - [$addedErrors, $argumentsWithParameters] = $this->processArguments($parametersAcceptor, $funcCall->getStartLine(), $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; } @@ -289,20 +328,38 @@ public function check( } } + 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 && $this->checkUnresolvableParameterTypes) // bleeding edge only + || (!$isBuiltin && !$argumentValueType instanceof ErrorType) ) { - $accepts = $this->ruleLevelHelper->acceptsWithReason($parameterType, $argumentValueType, $scope->isDeclareStrictTypes()); + $accepts = $this->ruleLevelHelper->accepts($parameterType, $argumentValueType, $scope->isDeclareStrictTypes()); if (!$accepts->result) { $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $argumentValueType); $errors[] = RuleErrorBuilder::message(sprintf( - $messages[6], - $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + $wrongArgumentTypeMessage, + $this->describeParameter($parameter, $argumentName ?? $i + 1), $parameterType->describe($verbosityLevel), $argumentValueType->describe($verbosityLevel), )) @@ -313,26 +370,25 @@ public function check( } } - if ($this->checkUnresolvableParameterTypes - && $originalParameter !== null - && isset($messages[13]) + if ( + $originalParameter !== null && !$this->unresolvableTypeHelper->containsUnresolvableType($originalParameter->getType()) && $this->unresolvableTypeHelper->containsUnresolvableType($parameterType) ) { $errors[] = RuleErrorBuilder::message(sprintf( - $messages[13], + $unresolvableParameterTypeMessage, $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), ))->identifier('argument.unresolvableType')->line($argumentLine)->build(); } if ( - $parameter instanceof ParameterReflectionWithPhpDocs + $parameter instanceof ExtendedParameterReflection && $parameter->getClosureThisType() !== null && ($argumentValue instanceof Expr\Closure || $argumentValue instanceof Expr\ArrowFunction) && $argumentValue->static ) { $errors[] = RuleErrorBuilder::message(sprintf( - $messages[6], + $wrongArgumentTypeMessage, $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), 'bindable closure', 'static closure', @@ -352,7 +408,7 @@ public function check( if ($this->nullsafeCheck->containsNullSafe($argumentValue)) { $errors[] = RuleErrorBuilder::message(sprintf( - $messages[8], + $parameterPassedByReferenceMessage, $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), )) ->identifier('argument.byRef') @@ -370,18 +426,27 @@ public function check( if ($nativePropertyReflection === null) { continue; } - if (!$nativePropertyReflection->isReadOnly()) { - continue; - } - if ($nativePropertyReflection->isStatic()) { - $propertyDescription = sprintf('static readonly property %s::$%s', $propertyReflection->getDeclaringClass()->getDisplayName(), $propertyReflection->getName()); + if ($nativePropertyReflection->isReadOnly()) { + if ($nativePropertyReflection->isStatic()) { + $errorFormat = 'static readonly property %s::$%s'; + } else { + $errorFormat = 'readonly property %s::$%s'; + } + } elseif ($nativePropertyReflection->isReadOnlyByPhpDoc()) { + if ($nativePropertyReflection->isStatic()) { + $errorFormat = 'static @readonly property %s::$%s'; + } else { + $errorFormat = '@readonly property %s::$%s'; + } } else { - $propertyDescription = sprintf('readonly property %s::$%s', $propertyReflection->getDeclaringClass()->getDisplayName(), $propertyReflection->getName()); + continue; } + $propertyDescription = sprintf($errorFormat, $propertyReflection->getDeclaringClass()->getDisplayName(), $propertyReflection->getName()); + $errors[] = RuleErrorBuilder::message(sprintf( - 'Parameter %s is passed by reference so it does not accept %s.', + '%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(); @@ -396,7 +461,7 @@ public function check( } $errors[] = RuleErrorBuilder::message(sprintf( - $messages[8], + $parameterPassedByReferenceMessage, $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), ))->identifier('argument.byRef')->line($argumentLine)->build(); } @@ -413,7 +478,7 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty $type = $type->resolve(); } - if ($type instanceof TemplateType) { + if ($type instanceof TemplateType && $type->getDefault() === null) { $returnTemplateTypes[$type->getName()] = true; return $type; } @@ -425,7 +490,7 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty $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; } @@ -453,7 +518,7 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty continue; } - $errors[] = RuleErrorBuilder::message(sprintf($messages[9], $name)) + $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') @@ -465,7 +530,7 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty !$this->unresolvableTypeHelper->containsUnresolvableType($originalParametersAcceptor->getReturnType()) && $this->unresolvableTypeHelper->containsUnresolvableType($parametersAcceptor->getReturnType()) ) { - $errors[] = RuleErrorBuilder::message($messages[12]) + $errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage) ->identifier(sprintf('%s.unresolvableReturnType', $nodeType)) ->line($funcCall->getStartLine()) ->build(); @@ -597,11 +662,15 @@ private function processArguments( return [$errors, $newArguments]; } - private function describeParameter(ParameterReflection $parameter, ?int $position): string + private function describeParameter(ParameterReflection $parameter, int|string|null $positionOrNamed): string { $parts = []; - if ($position !== null) { - $parts[] = '#' . $position; + 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(); diff --git a/src/Rules/FunctionDefinitionCheck.php b/src/Rules/FunctionDefinitionCheck.php index d2bdef50ba..21c61db5c8 100644 --- a/src/Rules/FunctionDefinitionCheck.php +++ b/src/Rules/FunctionDefinitionCheck.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules; use PhpParser\Node; +use PhpParser\Node\ComplexType; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\Variable; use PhpParser\Node\FunctionLike; @@ -15,13 +16,15 @@ 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; @@ -41,8 +44,10 @@ use function in_array; use function is_string; use function sprintf; +use function strtolower; -class FunctionDefinitionCheck +#[AutowiredService] +final class FunctionDefinitionCheck { public function __construct( @@ -50,7 +55,9 @@ public function __construct( private ClassNameCheck $classCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, private PhpVersion $phpVersion, + #[AutowiredParameter] private bool $checkClassCaseSensitivity, + #[AutowiredParameter] private bool $checkThisOnly, ) { @@ -60,8 +67,9 @@ public function __construct( * @return list */ public function checkFunction( + Scope $scope, Function_ $function, - FunctionReflection $functionReflection, + PhpFunctionFromParserNodeReflection $functionReflection, string $parameterMessage, string $returnMessage, string $unionTypesMessage, @@ -70,10 +78,9 @@ public function checkFunction( string $unresolvableReturnTypeMessage, ): array { - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()); - return $this->checkParametersAcceptor( - $parametersAcceptor, + $scope, + $functionReflection, $function, $parameterMessage, $returnMessage, @@ -102,7 +109,7 @@ public function checkAnonymousFunction( { $errors = []; $unionTypeReported = false; - foreach ($parameters as $param) { + foreach ($parameters as $i => $param) { if ($param->type === null) { continue; } @@ -122,6 +129,18 @@ public function checkAnonymousFunction( if (!$param->var instanceof Variable || !is_string($param->var->name)) { throw new ShouldNotHappenException(); } + + $implicitlyNullableTypeError = $this->checkImplicitlyNullableType( + $param->type, + $param->default, + $i + 1, + $param->getStartLine(), + $param->var->name, + ); + if ($implicitlyNullableTypeError !== null) { + $errors[] = $implicitlyNullableTypeError; + } + $type = $scope->getFunctionType($param->type, false, false); if ($type->isVoid()->yes()) { $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $param->var->name, 'void')) @@ -161,9 +180,12 @@ public function checkAnonymousFunction( $errors = array_merge( $errors, - $this->classCheck->checkClassNames([ + $this->classCheck->checkClassNames($scope, [ new ClassNameNodePair($class, $param->type), - ], $this->checkClassCaseSensitivity), + ], ClassNameUsageLocation::from(ClassNameUsageLocation::PARAMETER_TYPE, [ + 'parameterName' => $param->var->name, + 'isInAnonymousFunction' => true, + ]), $this->checkClassCaseSensitivity), ); } } @@ -203,7 +225,7 @@ public function checkAnonymousFunction( foreach ($returnType->getReferencedClasses() as $returnTypeClass) { if (!$this->reflectionProvider->hasClass($returnTypeClass)) { $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $returnTypeClass)) - ->line($returnTypeNode->getLine()) + ->line($returnTypeNode->getStartLine()) ->identifier('class.notFound') ->build(); continue; @@ -219,9 +241,11 @@ public function checkAnonymousFunction( $errors = array_merge( $errors, - $this->classCheck->checkClassNames([ + $this->classCheck->checkClassNames($scope, [ new ClassNameNodePair($returnTypeClass, $returnTypeNode), - ], $this->checkClassCaseSensitivity), + ], ClassNameUsageLocation::from(ClassNameUsageLocation::RETURN_TYPE, [ + 'isInAnonymousFunction' => true, + ]), $this->checkClassCaseSensitivity), ); } @@ -232,21 +256,21 @@ public function checkAnonymousFunction( * @return list */ public function checkClassMethod( + Scope $scope, PhpMethodFromParserNodeReflection $methodReflection, - ClassMethod $methodNode, + ClassMethod|Node\PropertyHook $methodNode, string $parameterMessage, string $returnMessage, string $unionTypesMessage, string $templateTypeMissingInParameterMessage, string $unresolvableParameterTypeMessage, string $unresolvableReturnTypeMessage, + string $selfOutMessage, ): array { - /** @var ParametersAcceptorWithPhpDocs $parametersAcceptor */ - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants()); - - return $this->checkParametersAcceptor( - $parametersAcceptor, + $errors = $this->checkParametersAcceptor( + $scope, + $methodReflection, $methodNode, $parameterMessage, $returnMessage, @@ -255,13 +279,49 @@ public function checkClassMethod( $unresolvableParameterTypeMessage, $unresolvableReturnTypeMessage, ); + + $selfOutType = $methodReflection->getSelfOutType(); + if ($selfOutType !== null) { + $selfOutTypeReferencedClasses = $selfOutType->getReferencedClasses(); + + foreach ($selfOutTypeReferencedClasses as $class) { + if (!$this->reflectionProvider->hasClass($class)) { + $errors[] = RuleErrorBuilder::message(sprintf($selfOutMessage, $class)) + ->line($methodNode->getStartLine()) + ->identifier('class.notFound') + ->build(); + continue; + } + if (!$this->reflectionProvider->getClass($class)->isTrait()) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf($selfOutMessage, $class)) + ->line($methodNode->getStartLine()) + ->identifier('selfOut.trait') + ->build(); + } + + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames( + $scope, + array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $methodNode), $selfOutTypeReferencedClasses), + ClassNameUsageLocation::from(ClassNameUsageLocation::PHPDOC_TAG_SELF_OUT), + $this->checkClassCaseSensitivity, + ), + ); + } + + return $errors; } /** * @return list */ private function checkParametersAcceptor( - ParametersAcceptor $parametersAcceptor, + Scope $scope, + PhpMethodFromParserNodeReflection|PhpFunctionFromParserNodeReflection $parametersAcceptor, FunctionLike $functionNode, string $parameterMessage, string $returnMessage, @@ -298,6 +358,18 @@ private function checkParametersAcceptor( } } + foreach ($parameterNodes as $i => $parameterNode) { + if (!$parameterNode->var instanceof Variable || !is_string($parameterNode->var->name)) { + throw new ShouldNotHappenException(); + } + $implicitlyNullableTypeError = $this->checkImplicitlyNullableType($parameterNode->type, $parameterNode->default, $i + 1, $parameterNode->getStartLine(), $parameterNode->var->name); + if ($implicitlyNullableTypeError === null) { + continue; + } + + $errors[] = $implicitlyNullableTypeError; + } + if ($this->phpVersion->deprecatesRequiredParameterAfterOptional()) { $errors = array_merge($errors, $this->checkRequiredParameterAfterOptional($parameterNodes)); } @@ -313,28 +385,26 @@ private function checkParametersAcceptor( return $parameterNode; }; - if ($parameter instanceof ParameterReflectionWithPhpDocs) { - $parameterVar = $parameterNodeCallback()->var; - if (!$parameterVar instanceof Variable || !is_string($parameterVar->name)) { - throw new ShouldNotHappenException(); - } - if ($parameter->getNativeType()->isVoid()->yes()) { - $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $parameterVar->name, 'void')) - ->line($parameterNodeCallback()->getStartLine()) - ->identifier('parameter.void') - ->nonIgnorable() - ->build(); - } - if ( - $this->phpVersion->supportsPureIntersectionTypes() - && $this->unresolvableTypeHelper->containsUnresolvableType($parameter->getNativeType()) - ) { - $errors[] = RuleErrorBuilder::message(sprintf($unresolvableParameterTypeMessage, $parameterVar->name)) - ->line($parameterNodeCallback()->getStartLine()) - ->identifier('parameter.unresolvableNativeType') - ->nonIgnorable() - ->build(); - } + $parameterVar = $parameterNodeCallback()->var; + if (!$parameterVar instanceof Variable || !is_string($parameterVar->name)) { + throw new ShouldNotHappenException(); + } + if ($parameter->getNativeType()->isVoid()->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $parameterVar->name, 'void')) + ->line($parameterNodeCallback()->getStartLine()) + ->identifier('parameter.void') + ->nonIgnorable() + ->build(); + } + if ( + $this->phpVersion->supportsPureIntersectionTypes() + && $this->unresolvableTypeHelper->containsUnresolvableType($parameter->getNativeType()) + ) { + $errors[] = RuleErrorBuilder::message(sprintf($unresolvableParameterTypeMessage, $parameterVar->name)) + ->line($parameterNodeCallback()->getStartLine()) + ->identifier('parameter.unresolvableNativeType') + ->nonIgnorable() + ->build(); } foreach ($referencedClasses as $class) { if (!$this->reflectionProvider->hasClass($class)) { @@ -362,10 +432,24 @@ private function checkParametersAcceptor( ->build(); } + $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, ), ); @@ -410,10 +494,22 @@ private function checkParametersAcceptor( ->build(); } + $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, ), ); @@ -575,7 +671,7 @@ private function getParameterNode( */ private function getParameterReferencedClasses(ParameterReflection $parameter): array { - if (!$parameter instanceof ParameterReflectionWithPhpDocs) { + if (!$parameter instanceof ExtendedParameterReflection) { return $parameter->getType()->getReferencedClasses(); } @@ -583,9 +679,18 @@ private function getParameterReferencedClasses(ParameterReflection $parameter): return $parameter->getNativeType()->getReferencedClasses(); } + $moreClasses = []; + if ($parameter->getOutType() !== null) { + $moreClasses = array_merge($moreClasses, $parameter->getOutType()->getReferencedClasses()); + } + if ($parameter->getClosureThisType() !== null) { + $moreClasses = array_merge($moreClasses, $parameter->getClosureThisType()->getReferencedClasses()); + } + return array_merge( $parameter->getNativeType()->getReferencedClasses(), $parameter->getPhpDocType()->getReferencedClasses(), + $moreClasses, ); } @@ -594,7 +699,7 @@ private function getParameterReferencedClasses(ParameterReflection $parameter): */ private function getReturnTypeReferencedClasses(ParametersAcceptor $parametersAcceptor): array { - if (!$parametersAcceptor instanceof ParametersAcceptorWithPhpDocs) { + if (!$parametersAcceptor instanceof ExtendedParametersAcceptor) { return $parametersAcceptor->getReturnType()->getReferencedClasses(); } @@ -608,4 +713,61 @@ private function getReturnTypeReferencedClasses(ParametersAcceptor $parametersAc ); } + private function checkImplicitlyNullableType( + Identifier|Name|ComplexType|null $type, + ?Node\Expr $default, + int $order, + int $line, + string $name, + ): ?IdentifierRuleError + { + if (!$default instanceof ConstFetch) { + return null; + } + + if ($default->name->toLowerString() !== 'null') { + return null; + } + + if ($type === null) { + return null; + } + + if ($type instanceof NullableType || $type instanceof IntersectionType) { + return null; + } + + if (!$this->phpVersion->deprecatesImplicitlyNullableParameterTypes()) { + return null; + } + + if ($type instanceof Identifier && strtolower($type->name) === 'mixed') { + return null; + } + + if ($type instanceof Identifier && strtolower($type->name) === 'null') { + return null; + } + if ($type instanceof Name && $type->toLowerString() === 'null') { + return null; + } + + if ($type instanceof UnionType) { + foreach ($type->types as $innerType) { + if ($innerType instanceof Identifier && strtolower($innerType->name) === 'null') { + return null; + } + } + } + + return RuleErrorBuilder::message(sprintf( + 'Deprecated in PHP 8.4: Parameter #%d $%s (%s) is implicitly nullable via default value null.', + $order, + $name, + NodeTypePrinter::printType($type), + ))->line($line) + ->identifier('parameter.implicitlyNullable') + ->build(); + } + } diff --git a/src/Rules/FunctionReturnTypeCheck.php b/src/Rules/FunctionReturnTypeCheck.php index 82bb5d529b..e61965ca3a 100644 --- a/src/Rules/FunctionReturnTypeCheck.php +++ b/src/Rules/FunctionReturnTypeCheck.php @@ -6,6 +6,7 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\ErrorType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; @@ -13,7 +14,8 @@ use PHPStan\Type\VerbosityLevel; use function sprintf; -class FunctionReturnTypeCheck +#[AutowiredService] +final class FunctionReturnTypeCheck { public function __construct(private RuleLevelHelper $ruleLevelHelper) @@ -90,7 +92,7 @@ public function checkReturnType( ]; } - $accepts = $this->ruleLevelHelper->acceptsWithReason($returnType, $returnValueType, $scope->isDeclareStrictTypes()); + $accepts = $this->ruleLevelHelper->accepts($returnType, $returnValueType, $scope->isDeclareStrictTypes()); if (!$accepts->result) { return [ RuleErrorBuilder::message(sprintf( diff --git a/src/Rules/Functions/ArrayFilterRule.php b/src/Rules/Functions/ArrayFilterRule.php index 177d1d218d..f7c32cc2c7 100644 --- a/src/Rules/Functions/ArrayFilterRule.php +++ b/src/Rules/Functions/ArrayFilterRule.php @@ -6,6 +6,8 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; @@ -18,12 +20,16 @@ /** * @implements Rule */ -class ArrayFilterRule implements Rule +#[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, ) { } @@ -79,7 +85,7 @@ public function processNode(Node $node, Scope $scope): array ))->identifier('arrayFilter.empty'); if ($this->treatPhpDocTypesAsCertain) { $nativeArrayType = $scope->getNativeType($args[0]->value); - if (!$nativeArrayType->isIterableAtLeastOnce()->no()) { + if ($this->treatPhpDocTypesAsCertainTip && !$nativeArrayType->isIterableAtLeastOnce()->no()) { $errorBuilder->treatPhpDocTypesAsCertainTip(); } } @@ -101,7 +107,7 @@ public function processNode(Node $node, Scope $scope): array if ($this->treatPhpDocTypesAsCertain) { $nativeArrayType = $scope->getNativeType($args[0]->value); $isNativeSuperType = $falsyType->isSuperTypeOf($nativeArrayType->getIterableValueType()); - if (!$isNativeSuperType->no()) { + if ($this->treatPhpDocTypesAsCertainTip && !$isNativeSuperType->no()) { $errorBuilder->treatPhpDocTypesAsCertainTip(); } } @@ -121,7 +127,7 @@ public function processNode(Node $node, Scope $scope): array if ($this->treatPhpDocTypesAsCertain) { $nativeArrayType = $scope->getNativeType($args[0]->value); $isNativeSuperType = $falsyType->isSuperTypeOf($nativeArrayType->getIterableValueType()); - if (!$isNativeSuperType->yes()) { + if ($this->treatPhpDocTypesAsCertainTip && !$isNativeSuperType->yes()) { $errorBuilder->treatPhpDocTypesAsCertainTip(); } } diff --git a/src/Rules/Functions/ArrayValuesRule.php b/src/Rules/Functions/ArrayValuesRule.php index bb62442ec8..0285a5c983 100644 --- a/src/Rules/Functions/ArrayValuesRule.php +++ b/src/Rules/Functions/ArrayValuesRule.php @@ -6,11 +6,12 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\VerbosityLevel; use function count; use function sprintf; @@ -18,12 +19,16 @@ /** * @implements Rule */ -class ArrayValuesRule implements Rule +#[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, ) { } @@ -39,10 +44,6 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (AccessoryArrayListType::isListTypeEnabled() === false) { - return []; - } - if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { return []; } @@ -84,7 +85,7 @@ public function processNode(Node $node, Scope $scope): array ))->identifier('arrayValues.empty'); if ($this->treatPhpDocTypesAsCertain) { $nativeArrayType = $scope->getNativeType($args[0]->value); - if (!$nativeArrayType->isIterableAtLeastOnce()->no()) { + if ($this->treatPhpDocTypesAsCertainTip && !$nativeArrayType->isIterableAtLeastOnce()->no()) { $errorBuilder->treatPhpDocTypesAsCertainTip(); } } @@ -102,7 +103,7 @@ public function processNode(Node $node, Scope $scope): array ))->identifier('arrayValues.list'); if ($this->treatPhpDocTypesAsCertain) { $nativeArrayType = $scope->getNativeType($args[0]->value); - if (!$nativeArrayType->isList()->yes()) { + if ($this->treatPhpDocTypesAsCertainTip && !$nativeArrayType->isList()->yes()) { $errorBuilder->treatPhpDocTypesAsCertainTip(); } } diff --git a/src/Rules/Functions/ArrowFunctionAttributesRule.php b/src/Rules/Functions/ArrowFunctionAttributesRule.php index 1b4e1ddf03..092758eaad 100644 --- a/src/Rules/Functions/ArrowFunctionAttributesRule.php +++ b/src/Rules/Functions/ArrowFunctionAttributesRule.php @@ -5,13 +5,16 @@ 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 { public function __construct(private AttributesCheck $attributesCheck) @@ -20,14 +23,14 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Expr\ArrowFunction::class; + return InArrowFunctionNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, + $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', ); diff --git a/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php b/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php index 371f2fa61a..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,7 +12,8 @@ /** * @implements Rule */ -class ArrowFunctionReturnNullsafeByRefRule implements Rule +#[RegisteredRule(level: 0)] +final class ArrowFunctionReturnNullsafeByRefRule implements Rule { public function __construct(private NullsafeCheck $nullsafeCheck) diff --git a/src/Rules/Functions/ArrowFunctionReturnTypeRule.php b/src/Rules/Functions/ArrowFunctionReturnTypeRule.php index aa0a9436de..ab1fecfe73 100644 --- a/src/Rules/Functions/ArrowFunctionReturnTypeRule.php +++ b/src/Rules/Functions/ArrowFunctionReturnTypeRule.php @@ -5,18 +5,19 @@ 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\Type; /** * @implements Rule */ -class ArrowFunctionReturnTypeRule implements Rule +#[RegisteredRule(level: 3)] +final class ArrowFunctionReturnTypeRule implements Rule { public function __construct(private FunctionReturnTypeCheck $returnTypeCheck) @@ -34,7 +35,6 @@ public function processNode(Node $node, Scope $scope): array throw new ShouldNotHappenException(); } - /** @var Type $returnType */ $returnType = $scope->getAnonymousFunctionReturnType(); $generatorType = new ObjectType(Generator::class); diff --git a/src/Rules/Functions/CallCallablesRule.php b/src/Rules/Functions/CallCallablesRule.php index e5c0d7dc8c..4ab0af4b01 100644 --- a/src/Rules/Functions/CallCallablesRule.php +++ b/src/Rules/Functions/CallCallablesRule.php @@ -5,6 +5,8 @@ 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; @@ -12,6 +14,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\TrinaryLogic; use PHPStan\Type\ClosureType; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; @@ -24,12 +27,14 @@ /** * @implements Rule */ -class CallCallablesRule implements Rule +#[RegisteredRule(level: 2)] +final class CallCallablesRule implements Rule { public function __construct( private FunctionCallParametersCheck $check, private RuleLevelHelper $ruleLevelHelper, + #[AutowiredParameter] private bool $reportMaybes, ) { @@ -79,6 +84,11 @@ public function processNode( $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 @@ -112,23 +122,23 @@ public function processNode( $scope, false, $node, - [ - ucfirst($callableDescription) . ' invoked with %d parameter, %d required.', - ucfirst($callableDescription) . ' invoked with %d parameters, %d required.', - ucfirst($callableDescription) . ' invoked with %d parameter, at least %d required.', - ucfirst($callableDescription) . ' invoked with %d parameters, at least %d required.', - ucfirst($callableDescription) . ' invoked with %d parameter, %d-%d required.', - ucfirst($callableDescription) . ' invoked with %d parameters, %d-%d required.', - 'Parameter %s of ' . $callableDescription . ' expects %s, %s given.', - 'Result of ' . $callableDescription . ' (void) is used.', - 'Parameter %s of ' . $callableDescription . ' is passed by reference, so it expects variables only.', - 'Unable to resolve the template type %s in call to ' . $callableDescription, - 'Missing parameter $%s in call to ' . $callableDescription . '.', - 'Unknown parameter $%s in call to ' . $callableDescription . '.', - 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', - 'Parameter %s of ' . $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 01b80d7f68..39f6f7cfea 100644 --- a/src/Rules/Functions/CallToFunctionParametersRule.php +++ b/src/Rules/Functions/CallToFunctionParametersRule.php @@ -5,6 +5,7 @@ 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; @@ -14,7 +15,8 @@ /** * @implements Rule */ -class CallToFunctionParametersRule implements Rule +#[RegisteredRule(level: 0)] +final class CallToFunctionParametersRule implements Rule { public function __construct(private ReflectionProvider $reflectionProvider, private FunctionCallParametersCheck $check) @@ -49,23 +51,23 @@ public function processNode(Node $node, Scope $scope): array $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.', - 'Parameter %s of function ' . $functionName . ' contains unresolvable type.', - ], 'function', + $function->acceptsNamedArguments(), + 'Function ' . $functionName . ' invoked with %d parameter, %d required.', + 'Function ' . $functionName . ' invoked with %d parameters, %d required.', + 'Function ' . $functionName . ' invoked with %d parameter, at least %d required.', + 'Function ' . $functionName . ' invoked with %d parameters, at least %d required.', + 'Function ' . $functionName . ' invoked with %d parameter, %d-%d required.', + 'Function ' . $functionName . ' invoked with %d parameters, %d-%d required.', + '%s of function ' . $functionName . ' expects %s, %s given.', + 'Result of function ' . $functionName . ' (void) is used.', + '%s of function ' . $functionName . ' is passed by reference, so it expects variables only.', + 'Unable to resolve the template type %s in call to function ' . $functionName, + 'Missing parameter $%s in call to function ' . $functionName . '.', + 'Unknown parameter $%s in call to function ' . $functionName . '.', + 'Return type of call to function ' . $functionName . ' contains unresolvable type.', + '%s of function ' . $functionName . ' contains unresolvable type.', + 'Function ' . $functionName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', ); } diff --git a/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php index 685fa41de0..e7d9c46156 100644 --- a/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php +++ b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php @@ -5,6 +5,7 @@ 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; @@ -16,7 +17,8 @@ /** * @implements Rule */ -class CallToFunctionStatementWithoutSideEffectsRule implements Rule +#[RegisteredRule(level: 4)] +final class CallToFunctionStatementWithoutSideEffectsRule implements Rule { private const SIDE_EFFECT_FLIP_PARAMETERS = [ @@ -28,9 +30,13 @@ class CallToFunctionStatementWithoutSideEffectsRule implements Rule ]; 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', ]; diff --git a/src/Rules/Functions/CallToNonExistentFunctionRule.php b/src/Rules/Functions/CallToNonExistentFunctionRule.php index 72fca5073c..88f98f4927 100644 --- a/src/Rules/Functions/CallToNonExistentFunctionRule.php +++ b/src/Rules/Functions/CallToNonExistentFunctionRule.php @@ -5,6 +5,8 @@ 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; @@ -14,12 +16,16 @@ /** * @implements Rule */ -class CallToNonExistentFunctionRule implements Rule +#[RegisteredRule(level: 0)] +final class CallToNonExistentFunctionRule implements Rule { public function __construct( private ReflectionProvider $reflectionProvider, + #[AutowiredParameter] private bool $checkFunctionNameCase, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { } @@ -40,8 +46,15 @@ public function processNode(Node $node, Scope $scope): array return []; } + $errorBuilder = RuleErrorBuilder::message(sprintf('Function %s not found.', (string) $node->name)) + ->identifier('function.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + return [ - RuleErrorBuilder::message(sprintf('Function %s not found.', (string) $node->name))->identifier('function.notFound')->discoveringSymbolsTip()->build(), + $errorBuilder->build(), ]; } diff --git a/src/Rules/Functions/CallUserFuncRule.php b/src/Rules/Functions/CallUserFuncRule.php index 040a5aecce..3dae092b52 100644 --- a/src/Rules/Functions/CallUserFuncRule.php +++ b/src/Rules/Functions/CallUserFuncRule.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\Rule; @@ -15,7 +16,8 @@ /** * @implements Rule */ -class CallUserFuncRule implements Rule +#[RegisteredRule(level: 5)] +final class CallUserFuncRule implements Rule { public function __construct( @@ -56,26 +58,33 @@ public function processNode(Node $node, Scope $scope): array if ($result === null) { return []; } - [$parametersAcceptor, $funcCall] = $result; + [$parametersAcceptor, $funcCall, $acceptsNamedArguments] = $result; $callableDescription = 'callable passed to call_user_func()'; - return $this->check->check($parametersAcceptor, $scope, false, $funcCall, [ + 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.', - 'Parameter %s of ' . $callableDescription . ' expects %s, %s given.', + '%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.', + '%s of ' . $callableDescription . ' is passed by reference, so it expects variables only.', 'Unable to resolve the template type %s in call to ' . $callableDescription, 'Missing parameter $%s in call to ' . $callableDescription . '.', 'Unknown parameter $%s in call to ' . $callableDescription . '.', 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', - 'Parameter %s of ' . $callableDescription . ' contains unresolvable type.', - ]); + '%s of ' . $callableDescription . ' contains unresolvable type.', + ucfirst($callableDescription) . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + ); } } diff --git a/src/Rules/Functions/ClosureAttributesRule.php b/src/Rules/Functions/ClosureAttributesRule.php index 170d9ad05e..d9dd348f9c 100644 --- a/src/Rules/Functions/ClosureAttributesRule.php +++ b/src/Rules/Functions/ClosureAttributesRule.php @@ -5,13 +5,16 @@ 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 { public function __construct(private AttributesCheck $attributesCheck) @@ -20,14 +23,14 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Expr\Closure::class; + return InClosureNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, + $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', ); diff --git a/src/Rules/Functions/ClosureReturnTypeRule.php b/src/Rules/Functions/ClosureReturnTypeRule.php index 98f72deb73..abeb7c3866 100644 --- a/src/Rules/Functions/ClosureReturnTypeRule.php +++ b/src/Rules/Functions/ClosureReturnTypeRule.php @@ -4,16 +4,17 @@ 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\Type; use PHPStan\Type\TypeCombinator; /** * @implements Rule */ -class ClosureReturnTypeRule implements Rule +#[RegisteredRule(level: 3)] +final class ClosureReturnTypeRule implements Rule { public function __construct(private FunctionReturnTypeCheck $returnTypeCheck) @@ -31,7 +32,6 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var Type $returnType */ $returnType = $scope->getAnonymousFunctionReturnType(); $containsNull = TypeCombinator::containsNull($returnType); $hasNativeTypehint = $node->getClosureExpr()->returnType !== null; diff --git a/src/Rules/Functions/DefineParametersRule.php b/src/Rules/Functions/DefineParametersRule.php index 5aac3ee5c2..3534ebc70b 100644 --- a/src/Rules/Functions/DefineParametersRule.php +++ b/src/Rules/Functions/DefineParametersRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,7 +15,8 @@ /** * @implements Rule */ -class DefineParametersRule implements Rule +#[RegisteredRule(level: 0)] +final class DefineParametersRule implements Rule { public function __construct(private PhpVersion $phpVersion) diff --git a/src/Rules/Functions/DuplicateFunctionDeclarationRule.php b/src/Rules/Functions/DuplicateFunctionDeclarationRule.php index 06d92643ae..6cf2b6c5c1 100644 --- a/src/Rules/Functions/DuplicateFunctionDeclarationRule.php +++ b/src/Rules/Functions/DuplicateFunctionDeclarationRule.php @@ -18,7 +18,7 @@ /** * @implements Rule */ -class DuplicateFunctionDeclarationRule implements Rule +final class DuplicateFunctionDeclarationRule implements Rule { public function __construct(private Reflector $reflector, private RelativePathHelper $relativePathHelper) diff --git a/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php index a2107c73a0..c2a18addbc 100644 --- a/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\Rule; @@ -15,7 +16,8 @@ /** * @implements Rule */ -class ExistingClassesInArrowFunctionTypehintsRule implements Rule +#[RegisteredRule(level: 0)] +final class ExistingClassesInArrowFunctionTypehintsRule implements Rule { public function __construct(private FunctionDefinitionCheck $check, private PhpVersion $phpVersion) diff --git a/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php b/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php index 2c6dbd3ad0..a052390041 100644 --- a/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php @@ -5,13 +5,15 @@ 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 Rule */ -class ExistingClassesInClosureTypehintsRule implements Rule +#[RegisteredRule(level: 0)] +final class ExistingClassesInClosureTypehintsRule implements Rule { public function __construct(private FunctionDefinitionCheck $check) diff --git a/src/Rules/Functions/ExistingClassesInTypehintsRule.php b/src/Rules/Functions/ExistingClassesInTypehintsRule.php index 6a9586a820..403ed4ec1a 100644 --- a/src/Rules/Functions/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInTypehintsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InFunctionNode; use PHPStan\Rules\FunctionDefinitionCheck; @@ -13,7 +14,8 @@ /** * @implements Rule */ -class ExistingClassesInTypehintsRule implements Rule +#[RegisteredRule(level: 0)] +final class ExistingClassesInTypehintsRule implements Rule { public function __construct(private FunctionDefinitionCheck $check) @@ -30,6 +32,7 @@ public function processNode(Node $node, Scope $scope): array $functionName = SprintfHelper::escapeFormatString($node->getFunctionReflection()->getName()); return $this->check->checkFunction( + $scope, $node->getOriginalNode(), $node->getFunctionReflection(), sprintf( diff --git a/src/Rules/Functions/FunctionAttributesRule.php b/src/Rules/Functions/FunctionAttributesRule.php index 2ccf122ac2..a7b6547cb0 100644 --- a/src/Rules/Functions/FunctionAttributesRule.php +++ b/src/Rules/Functions/FunctionAttributesRule.php @@ -5,13 +5,16 @@ 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 { public function __construct(private AttributesCheck $attributesCheck) @@ -20,14 +23,14 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Stmt\Function_::class; + return InFunctionNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, + $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', ); diff --git a/src/Rules/Functions/FunctionCallableRule.php b/src/Rules/Functions/FunctionCallableRule.php index fdecd2073f..a4b698d357 100644 --- a/src/Rules/Functions/FunctionCallableRule.php +++ b/src/Rules/Functions/FunctionCallableRule.php @@ -5,6 +5,8 @@ use PhpParser\Node; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\FunctionCallableNode; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; @@ -20,10 +22,19 @@ /** * @implements Rule */ -class FunctionCallableRule implements Rule +#[RegisteredRule(level: 0)] +final class FunctionCallableRule implements Rule { - public function __construct(private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, private PhpVersion $phpVersion, private bool $checkFunctionNameCase, private bool $reportMaybes) + public function __construct( + private ReflectionProvider $reflectionProvider, + private RuleLevelHelper $ruleLevelHelper, + private PhpVersion $phpVersion, + #[AutowiredParameter] + private bool $checkFunctionNameCase, + #[AutowiredParameter] + private bool $reportMaybes, + ) { } diff --git a/src/Rules/Functions/ImplodeFunctionRule.php b/src/Rules/Functions/ImplodeFunctionRule.php deleted file mode 100644 index b890a8ff66..0000000000 --- a/src/Rules/Functions/ImplodeFunctionRule.php +++ /dev/null @@ -1,84 +0,0 @@ - - */ -class ImplodeFunctionRule implements Rule -{ - - public function __construct( - private ReflectionProvider $reflectionProvider, - private RuleLevelHelper $ruleLevelHelper, - private bool $disabled, - ) - { - } - - public function getNodeType(): string - { - return FuncCall::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if ($this->disabled) { - return []; - } - - if (!($node->name instanceof Node\Name)) { - return []; - } - - $functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope); - if (!in_array($functionName, ['implode', 'join'], true)) { - return []; - } - - $args = $node->getArgs(); - if (count($args) === 1) { - $arrayArg = $args[0]->value; - $paramNo = 1; - } elseif (count($args) === 2) { - $arrayArg = $args[1]->value; - $paramNo = 2; - } else { - return []; - } - - $typeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $arrayArg, - '', - static fn (Type $type): bool => !$type->getIterableValueType()->toString() instanceof ErrorType, - ); - - if ($typeResult->getType() instanceof ErrorType - || !$typeResult->getType()->getIterableValueType()->toString() instanceof ErrorType) { - return []; - } - - return [ - RuleErrorBuilder::message( - sprintf('Parameter #%d $array of function %s expects array, %s given.', $paramNo, $functionName, $typeResult->getType()->describe(VerbosityLevel::typeOnly())), - )->identifier('argument.type')->build(), - ]; - } - -} diff --git a/src/Rules/Functions/ImplodeParameterCastableToStringRule.php b/src/Rules/Functions/ImplodeParameterCastableToStringRule.php index df5136a808..f58a4ca2be 100644 --- a/src/Rules/Functions/ImplodeParameterCastableToStringRule.php +++ b/src/Rules/Functions/ImplodeParameterCastableToStringRule.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ParameterCastableToStringCheck; @@ -19,7 +20,8 @@ /** * @implements Rule */ -class ImplodeParameterCastableToStringRule implements Rule +#[RegisteredRule(level: 5)] +final class ImplodeParameterCastableToStringRule implements Rule { public function __construct( diff --git a/src/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRule.php b/src/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRule.php index 585b00137a..ec31655fc5 100644 --- a/src/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRule.php +++ b/src/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InArrowFunctionNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -16,7 +17,8 @@ /** * @implements Rule */ -class IncompatibleArrowFunctionDefaultParameterTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class IncompatibleArrowFunctionDefaultParameterTypeRule implements Rule { public function getNodeType(): string @@ -44,7 +46,7 @@ public function processNode(Node $node, Scope $scope): array $parameterType = $parameters[$paramI]->getType(); $parameterType = TemplateTypeHelper::resolveToBounds($parameterType); - $accepts = $parameterType->acceptsWithReason($defaultValueType, true); + $accepts = $parameterType->accepts($defaultValueType, true); if ($accepts->yes()) { continue; } diff --git a/src/Rules/Functions/IncompatibleClosureDefaultParameterTypeRule.php b/src/Rules/Functions/IncompatibleClosureDefaultParameterTypeRule.php index 3cd1c51390..9562e5086b 100644 --- a/src/Rules/Functions/IncompatibleClosureDefaultParameterTypeRule.php +++ b/src/Rules/Functions/IncompatibleClosureDefaultParameterTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClosureNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -16,7 +17,8 @@ /** * @implements Rule */ -class IncompatibleClosureDefaultParameterTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class IncompatibleClosureDefaultParameterTypeRule implements Rule { public function getNodeType(): string @@ -44,7 +46,7 @@ public function processNode(Node $node, Scope $scope): array $parameterType = $parameters[$paramI]->getType(); $parameterType = TemplateTypeHelper::resolveToBounds($parameterType); - $accepts = $parameterType->acceptsWithReason($defaultValueType, true); + $accepts = $parameterType->accepts($defaultValueType, true); if ($accepts->yes()) { continue; } diff --git a/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php b/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php index a142269a68..6deb4c3f45 100644 --- a/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php +++ b/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php @@ -4,8 +4,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InFunctionNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; @@ -17,7 +17,8 @@ /** * @implements Rule */ -class IncompatibleDefaultParameterTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class IncompatibleDefaultParameterTypeRule implements Rule { public function getNodeType(): string @@ -28,8 +29,6 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $function = $node->getFunctionReflection(); - $parameters = ParametersAcceptorSelector::selectSingle($function->getVariants()); - $errors = []; foreach ($node->getOriginalNode()->getParams() as $paramI => $param) { if ($param->default === null) { @@ -43,10 +42,10 @@ public function processNode(Node $node, Scope $scope): array } $defaultValueType = $scope->getType($param->default); - $parameterType = $parameters->getParameters()[$paramI]->getType(); + $parameterType = $function->getParameters()[$paramI]->getType(); $parameterType = TemplateTypeHelper::resolveToBounds($parameterType); - $accepts = $parameterType->acceptsWithReason($defaultValueType, true); + $accepts = $parameterType->accepts($defaultValueType, true); if ($accepts->yes()) { continue; } diff --git a/src/Rules/Functions/InnerFunctionRule.php b/src/Rules/Functions/InnerFunctionRule.php index 24118f5fbb..b763b492e5 100644 --- a/src/Rules/Functions/InnerFunctionRule.php +++ b/src/Rules/Functions/InnerFunctionRule.php @@ -5,13 +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 Rule */ -class InnerFunctionRule implements Rule +#[RegisteredRule(level: 0)] +final class InnerFunctionRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Functions/InvalidLexicalVariablesInClosureUseRule.php b/src/Rules/Functions/InvalidLexicalVariablesInClosureUseRule.php index d722abffe1..eab6958f68 100644 --- a/src/Rules/Functions/InvalidLexicalVariablesInClosureUseRule.php +++ b/src/Rules/Functions/InvalidLexicalVariablesInClosureUseRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function array_filter; @@ -15,7 +16,8 @@ /** * @implements Rule */ -class InvalidLexicalVariablesInClosureUseRule implements Rule +#[RegisteredRule(level: 0)] +final class InvalidLexicalVariablesInClosureUseRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php index 73782fcf53..908430a5bb 100644 --- a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php @@ -4,9 +4,9 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InFunctionNode; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; @@ -14,18 +14,17 @@ use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** * @implements Rule */ +#[RegisteredRule(level: 6)] final class MissingFunctionParameterTypehintRule implements Rule { public function __construct( private MissingTypehintCheck $missingTypehintCheck, - private bool $paramOut, ) { } @@ -40,7 +39,7 @@ public function processNode(Node $node, Scope $scope): array $functionReflection = $node->getFunctionReflection(); $messages = []; - foreach (ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getParameters() as $parameterReflection) { + foreach ($functionReflection->getParameters() as $parameterReflection) { foreach ($this->checkFunctionParameter($functionReflection, sprintf('parameter $%s', $parameterReflection->getName()), $parameterReflection->getType()) as $parameterMessage) { $messages[] = $parameterMessage; } @@ -51,9 +50,6 @@ public function processNode(Node $node, Scope $scope): array } } - if (!$this->paramOut) { - continue; - } if ($parameterReflection->getOutType() === null) { continue; } @@ -101,7 +97,7 @@ private function checkFunctionParameter(FunctionReflection $functionReflection, $functionReflection->getName(), $parameterMessage, $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Functions/MissingFunctionReturnTypehintRule.php b/src/Rules/Functions/MissingFunctionReturnTypehintRule.php index 1546a63219..761a8fed31 100644 --- a/src/Rules/Functions/MissingFunctionReturnTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionReturnTypehintRule.php @@ -4,19 +4,19 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InFunctionNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** * @implements Rule */ +#[RegisteredRule(level: 6)] final class MissingFunctionReturnTypehintRule implements Rule { @@ -34,7 +34,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $functionReflection = $node->getFunctionReflection(); - $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $returnType = $functionReflection->getReturnType(); if ($returnType instanceof MixedType && !$returnType->isExplicitMixed()) { return [ @@ -59,7 +59,7 @@ public function processNode(Node $node, Scope $scope): array 'Function %s() return type with generic %s does not specify its types: %s', $functionReflection->getName(), $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Functions/ParamAttributesRule.php b/src/Rules/Functions/ParamAttributesRule.php index ee9e8f257c..ad67abb22b 100644 --- a/src/Rules/Functions/ParamAttributesRule.php +++ b/src/Rules/Functions/ParamAttributesRule.php @@ -5,13 +5,15 @@ 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 { public function __construct(private AttributesCheck $attributesCheck) 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 index b6b26da76e..bbc6e0be36 100644 --- a/src/Rules/Functions/ParameterCastableToStringRule.php +++ b/src/Rules/Functions/ParameterCastableToStringRule.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ParameterCastableToStringCheck; @@ -17,7 +18,8 @@ /** * @implements Rule */ -class ParameterCastableToStringRule implements Rule +#[RegisteredRule(level: 5)] +final class ParameterCastableToStringRule implements Rule { public function __construct( diff --git a/src/Rules/Functions/PrintfArrayParametersRule.php b/src/Rules/Functions/PrintfArrayParametersRule.php index d06cdc8f4a..1d7e093116 100644 --- a/src/Rules/Functions/PrintfArrayParametersRule.php +++ b/src/Rules/Functions/PrintfArrayParametersRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -22,7 +23,8 @@ /** * @implements Rule */ -class PrintfArrayParametersRule implements Rule +#[RegisteredRule(level: 0)] +final class PrintfArrayParametersRule implements Rule { public function __construct( diff --git a/src/Rules/Functions/PrintfHelper.php b/src/Rules/Functions/PrintfHelper.php index a5d4571f76..7f68f17fa4 100644 --- a/src/Rules/Functions/PrintfHelper.php +++ b/src/Rules/Functions/PrintfHelper.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Functions; use Nette\Utils\Strings; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use function array_filter; use function count; @@ -11,6 +12,7 @@ use function strlen; use const PREG_SET_ORDER; +#[AutowiredService] final class PrintfHelper { diff --git a/src/Rules/Functions/PrintfParametersRule.php b/src/Rules/Functions/PrintfParametersRule.php index a1d8e52f35..44b02b14e9 100644 --- a/src/Rules/Functions/PrintfParametersRule.php +++ b/src/Rules/Functions/PrintfParametersRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -16,7 +17,8 @@ /** * @implements Rule */ -class PrintfParametersRule implements Rule +#[RegisteredRule(level: 0)] +final class PrintfParametersRule implements Rule { private const FORMAT_ARGUMENT_POSITIONS = [ diff --git a/src/Rules/Functions/RandomIntParametersRule.php b/src/Rules/Functions/RandomIntParametersRule.php index cc9da2c3ed..d2f8c52597 100644 --- a/src/Rules/Functions/RandomIntParametersRule.php +++ b/src/Rules/Functions/RandomIntParametersRule.php @@ -5,6 +5,9 @@ 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; @@ -18,10 +21,16 @@ /** * @implements Rule */ -class RandomIntParametersRule implements Rule +#[RegisteredRule(level: 5)] +final class RandomIntParametersRule implements Rule { - public function __construct(private ReflectionProvider $reflectionProvider, private bool $reportMaybes) + public function __construct( + private ReflectionProvider $reflectionProvider, + private PhpVersion $phpVersion, + #[AutowiredParameter] + private bool $reportMaybes, + ) { } @@ -55,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).'; diff --git a/src/Rules/Functions/RedefinedParametersRule.php b/src/Rules/Functions/RedefinedParametersRule.php index f364c82b9d..e4c37614f4 100644 --- a/src/Rules/Functions/RedefinedParametersRule.php +++ b/src/Rules/Functions/RedefinedParametersRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function count; @@ -13,7 +14,8 @@ /** * @implements Rule */ -class RedefinedParametersRule implements Rule +#[RegisteredRule(level: 0)] +final class RedefinedParametersRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Functions/ReturnNullsafeByRefRule.php b/src/Rules/Functions/ReturnNullsafeByRefRule.php index 84f2c81f3c..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,7 +13,8 @@ /** * @implements Rule */ -class ReturnNullsafeByRefRule implements Rule +#[RegisteredRule(level: 0)] +final class ReturnNullsafeByRefRule implements Rule { public function __construct(private NullsafeCheck $nullsafeCheck) diff --git a/src/Rules/Functions/ReturnTypeRule.php b/src/Rules/Functions/ReturnTypeRule.php index ab9897713b..157f03f2b2 100644 --- a/src/Rules/Functions/ReturnTypeRule.php +++ b/src/Rules/Functions/ReturnTypeRule.php @@ -5,9 +5,8 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; -use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\FunctionReturnTypeCheck; use PHPStan\Rules\Rule; use function sprintf; @@ -15,7 +14,8 @@ /** * @implements Rule */ -class ReturnTypeRule implements Rule +#[RegisteredRule(level: 3)] +final class ReturnTypeRule implements Rule { public function __construct( @@ -40,16 +40,13 @@ public function processNode(Node $node, Scope $scope): array } $function = $scope->getFunction(); - if ( - !($function instanceof PhpFunctionFromParserNodeReflection) - || $function instanceof PhpMethodFromParserNodeReflection - ) { + if ($function instanceof MethodReflection) { return []; } return $this->returnTypeCheck->checkReturnType( $scope, - ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(), + $function->getReturnType(), $node->expr, $node, sprintf( diff --git a/src/Rules/Functions/SortParameterCastableToStringRule.php b/src/Rules/Functions/SortParameterCastableToStringRule.php index dc1d4b63cf..1625024bd6 100644 --- a/src/Rules/Functions/SortParameterCastableToStringRule.php +++ b/src/Rules/Functions/SortParameterCastableToStringRule.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ParameterCastableToStringCheck; @@ -26,7 +27,8 @@ /** * @implements Rule */ -class SortParameterCastableToStringRule implements Rule +#[RegisteredRule(level: 5)] +final class SortParameterCastableToStringRule implements Rule { public function __construct( diff --git a/src/Rules/Functions/UnusedClosureUsesRule.php b/src/Rules/Functions/UnusedClosureUsesRule.php index 0caa2c2ae9..8a4bb345b6 100644 --- a/src/Rules/Functions/UnusedClosureUsesRule.php +++ b/src/Rules/Functions/UnusedClosureUsesRule.php @@ -4,17 +4,17 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\UnusedFunctionParametersCheck; -use PHPStan\ShouldNotHappenException; use function array_map; use function count; -use function is_string; /** * @implements Rule */ -class UnusedClosureUsesRule implements Rule +#[RegisteredRule(level: 1)] +final class UnusedClosureUsesRule implements Rule { public function __construct(private UnusedFunctionParametersCheck $check) @@ -34,12 +34,7 @@ public function processNode(Node $node, Scope $scope): array return $this->check->getUnusedParameters( $scope, - array_map(static function (Node\Expr\ClosureUse $use): string { - if (!is_string($use->var->name)) { - throw new ShouldNotHappenException(); - } - return $use->var->name; - }, $node->uses), + array_map(static fn (Node\ClosureUse $use): Node\Expr\Variable => $use->var, $node->uses), $node->stmts, 'Anonymous function has an unused use $%s.', 'closure.unusedUse', diff --git a/src/Rules/Functions/UselessFunctionReturnValueRule.php b/src/Rules/Functions/UselessFunctionReturnValueRule.php index 102f9fec2f..92ef7d4e57 100644 --- a/src/Rules/Functions/UselessFunctionReturnValueRule.php +++ b/src/Rules/Functions/UselessFunctionReturnValueRule.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; @@ -17,7 +18,8 @@ /** * @implements Rule */ -class UselessFunctionReturnValueRule implements Rule +#[RegisteredRule(level: 4)] +final class UselessFunctionReturnValueRule implements Rule { private const USELESS_FUNCTIONS = [ diff --git a/src/Rules/Functions/VariadicParametersDeclarationRule.php b/src/Rules/Functions/VariadicParametersDeclarationRule.php index 2d7c048d80..73ca669ff1 100644 --- a/src/Rules/Functions/VariadicParametersDeclarationRule.php +++ b/src/Rules/Functions/VariadicParametersDeclarationRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function count; @@ -11,7 +12,8 @@ /** * @implements Rule */ -class VariadicParametersDeclarationRule implements Rule +#[RegisteredRule(level: 0)] +final class VariadicParametersDeclarationRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Generators/YieldFromTypeRule.php b/src/Rules/Generators/YieldFromTypeRule.php index deb3f8212e..b77f04b53a 100644 --- a/src/Rules/Generators/YieldFromTypeRule.php +++ b/src/Rules/Generators/YieldFromTypeRule.php @@ -6,7 +6,8 @@ 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; @@ -18,11 +19,13 @@ /** * @implements Rule */ -class YieldFromTypeRule implements Rule +#[RegisteredRule(level: 3)] +final class YieldFromTypeRule implements Rule { public function __construct( private RuleLevelHelper $ruleLevelHelper, + #[AutowiredParameter] private bool $reportMaybes, ) { @@ -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,7 +82,7 @@ public function processNode(Node $node, Scope $scope): array } $messages = []; - $acceptsKey = $this->ruleLevelHelper->acceptsWithReason($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( @@ -93,7 +96,7 @@ public function processNode(Node $node, Scope $scope): array ->build(); } - $acceptsValue = $this->ruleLevelHelper->acceptsWithReason($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( @@ -112,7 +115,7 @@ public function processNode(Node $node, Scope $scope): array return $messages; } - $currentReturnType = ParametersAcceptorSelector::selectSingle($scopeFunction->getVariants())->getReturnType(); + $currentReturnType = $scopeFunction->getReturnType(); $exprSendType = $exprType->getTemplateType(Generator::class, 'TSend'); $thisSendType = $currentReturnType->getTemplateType(Generator::class, 'TSend'); if ($exprSendType instanceof ErrorType || $thisSendType instanceof ErrorType) { diff --git a/src/Rules/Generators/YieldInGeneratorRule.php b/src/Rules/Generators/YieldInGeneratorRule.php index 9be06b937d..601c8f43a4 100644 --- a/src/Rules/Generators/YieldInGeneratorRule.php +++ b/src/Rules/Generators/YieldInGeneratorRule.php @@ -4,7 +4,8 @@ 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; @@ -15,10 +16,14 @@ /** * @implements Rule */ -class YieldInGeneratorRule implements Rule +#[RegisteredRule(level: 3)] +final class YieldInGeneratorRule implements Rule { - public function __construct(private bool $reportMaybes) + public function __construct( + #[AutowiredParameter] + private bool $reportMaybes, + ) { } @@ -38,7 +43,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 [ RuleErrorBuilder::message('Yield can be used only inside a function.') diff --git a/src/Rules/Generators/YieldTypeRule.php b/src/Rules/Generators/YieldTypeRule.php index 3de12e41a3..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; @@ -17,7 +17,8 @@ /** * @implements Rule */ -class YieldTypeRule implements Rule +#[RegisteredRule(level: 3)] +final class YieldTypeRule implements Rule { public function __construct( @@ -38,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 } @@ -54,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array } $messages = []; - $acceptsKey = $this->ruleLevelHelper->acceptsWithReason($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( @@ -73,7 +74,7 @@ public function processNode(Node $node, Scope $scope): array $valueType = $scope->getType($node->value); } - $acceptsValue = $this->ruleLevelHelper->acceptsWithReason($returnType->getIterableValueType(), $valueType, $scope->isDeclareStrictTypes()); + $acceptsValue = $this->ruleLevelHelper->accepts($returnType->getIterableValueType(), $valueType, $scope->isDeclareStrictTypes()); if (!$acceptsValue->result) { $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableValueType(), $valueType); $messages[] = RuleErrorBuilder::message(sprintf( diff --git a/src/Rules/Generics/ClassAncestorsRule.php b/src/Rules/Generics/ClassAncestorsRule.php index 0359073c44..9066de4f4b 100644 --- a/src/Rules/Generics/ClassAncestorsRule.php +++ b/src/Rules/Generics/ClassAncestorsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassNode; use PHPStan\PhpDoc\Tag\ExtendsTag; @@ -17,7 +18,8 @@ /** * @implements Rule */ -class ClassAncestorsRule implements Rule +#[RegisteredRule(level: 2)] +final class ClassAncestorsRule implements Rule { public function __construct( @@ -49,6 +51,7 @@ public function processNode(Node $node, Scope $scope): array $originalNode->extends !== null ? [$originalNode->extends] : [], array_map(static fn (ExtendsTag $tag): Type => $tag->getType(), $classReflection->getExtendsTags()), sprintf('Class %s @extends tag contains incompatible type %%s.', $escapedClassName), + sprintf('Class %s @extends tag contains unresolvable type.', $className), sprintf('Class %s has @extends tag, but does not extend any class.', $escapedClassName), sprintf('The @extends tag of class %s describes %%s but the class extends %%s.', $escapedClassName), 'PHPDoc tag @extends contains generic type %s but %s %s is not generic.', @@ -65,6 +68,7 @@ public function processNode(Node $node, Scope $scope): array $originalNode->implements, array_map(static fn (ImplementsTag $tag): Type => $tag->getType(), $classReflection->getImplementsTags()), sprintf('Class %s @implements tag contains incompatible type %%s.', $escapedClassName), + sprintf('Class %s @implements tag contains unresolvable type.', $className), sprintf('Class %s has @implements tag, but does not implement any interface.', $escapedClassName), sprintf('The @implements tag of class %s describes %%s but the class implements: %%s', $escapedClassName), 'PHPDoc tag @implements contains generic type %s but %s %s is not generic.', diff --git a/src/Rules/Generics/ClassTemplateTypeRule.php b/src/Rules/Generics/ClassTemplateTypeRule.php index a21d0561e5..b3515e927d 100644 --- a/src/Rules/Generics/ClassTemplateTypeRule.php +++ b/src/Rules/Generics/ClassTemplateTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; @@ -13,7 +14,8 @@ /** * @implements Rule */ -class ClassTemplateTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class ClassTemplateTypeRule implements Rule { public function __construct( @@ -49,6 +51,9 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template for %s cannot have existing type alias %%s as its name.', $displayName), sprintf('PHPDoc tag @template %%s for %s has invalid bound type %%s.', $displayName), sprintf('PHPDoc tag @template %%s for %s with bound type %%s is not supported.', $displayName), + sprintf('PHPDoc tag @template %%s for %s has invalid default type %%s.', $displayName), + sprintf('Default type %%s in PHPDoc tag @template %%s for %s is not subtype of bound type %%s.', $displayName), + sprintf('PHPDoc tag @template %%s for %s does not have a default type but follows an optional @template %%s.', $displayName), ); } diff --git a/src/Rules/Generics/CrossCheckInterfacesHelper.php b/src/Rules/Generics/CrossCheckInterfacesHelper.php index e35854b5ec..25b57e10e6 100644 --- a/src/Rules/Generics/CrossCheckInterfacesHelper.php +++ b/src/Rules/Generics/CrossCheckInterfacesHelper.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Generics; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; @@ -9,7 +10,8 @@ use function array_key_exists; use function sprintf; -class CrossCheckInterfacesHelper +#[AutowiredService] +final class CrossCheckInterfacesHelper { /** diff --git a/src/Rules/Generics/EnumAncestorsRule.php b/src/Rules/Generics/EnumAncestorsRule.php index 8c786d7359..67733187e3 100644 --- a/src/Rules/Generics/EnumAncestorsRule.php +++ b/src/Rules/Generics/EnumAncestorsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassNode; use PHPStan\PhpDoc\Tag\ExtendsTag; @@ -17,7 +18,8 @@ /** * @implements Rule */ -class EnumAncestorsRule implements Rule +#[RegisteredRule(level: 2)] +final class EnumAncestorsRule implements Rule { public function __construct( @@ -47,6 +49,7 @@ public function processNode(Node $node, Scope $scope): array [], array_map(static fn (ExtendsTag $tag): Type => $tag->getType(), $classReflection->getExtendsTags()), sprintf('Enum %s @extends tag contains incompatible type %%s.', $escapedEnumName), + sprintf('Enum %s @extends tag contains unresolvable type.', $enumName), sprintf('Enum %s has @extends tag, but cannot extend anything.', $escapedEnumName), '', '', @@ -63,6 +66,7 @@ public function processNode(Node $node, Scope $scope): array $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.', diff --git a/src/Rules/Generics/EnumTemplateTypeRule.php b/src/Rules/Generics/EnumTemplateTypeRule.php index 9c149811bf..29e9231f80 100644 --- a/src/Rules/Generics/EnumTemplateTypeRule.php +++ b/src/Rules/Generics/EnumTemplateTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -13,7 +14,8 @@ /** * @implements Rule */ -class EnumTemplateTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class EnumTemplateTypeRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Generics/FunctionSignatureVarianceRule.php b/src/Rules/Generics/FunctionSignatureVarianceRule.php index e95b2e7712..b25a263110 100644 --- a/src/Rules/Generics/FunctionSignatureVarianceRule.php +++ b/src/Rules/Generics/FunctionSignatureVarianceRule.php @@ -4,16 +4,17 @@ 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 Rule */ -class FunctionSignatureVarianceRule implements Rule +#[RegisteredRule(level: 2)] +final class FunctionSignatureVarianceRule implements Rule { public function __construct(private VarianceCheck $varianceCheck) @@ -31,7 +32,7 @@ public function processNode(Node $node, Scope $scope): array $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), diff --git a/src/Rules/Generics/FunctionTemplateTypeRule.php b/src/Rules/Generics/FunctionTemplateTypeRule.php index 3700dd3b73..29e9f927ec 100644 --- a/src/Rules/Generics/FunctionTemplateTypeRule.php +++ b/src/Rules/Generics/FunctionTemplateTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Rules\Rule; use PHPStan\ShouldNotHappenException; @@ -14,7 +15,8 @@ /** * @implements Rule */ -class FunctionTemplateTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class FunctionTemplateTypeRule implements Rule { public function __construct( @@ -60,6 +62,9 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template for function %s() cannot have existing type alias %%s as its name.', $escapedFunctionName), sprintf('PHPDoc tag @template %%s for function %s() has invalid bound type %%s.', $escapedFunctionName), sprintf('PHPDoc tag @template %%s for function %s() with bound type %%s is not supported.', $escapedFunctionName), + sprintf('PHPDoc tag @template %%s for function %s() has invalid default type %%s.', $escapedFunctionName), + sprintf('Default type %%s in PHPDoc tag @template %%s for function %s() is not subtype of bound type %%s.', $escapedFunctionName), + sprintf('PHPDoc tag @template %%s for function %s() does not have a default type but follows an optional @template %%s.', $escapedFunctionName), ); } diff --git a/src/Rules/Generics/GenericAncestorsCheck.php b/src/Rules/Generics/GenericAncestorsCheck.php index f78cbaf01c..56aff3b36f 100644 --- a/src/Rules/Generics/GenericAncestorsCheck.php +++ b/src/Rules/Generics/GenericAncestorsCheck.php @@ -4,15 +4,20 @@ use PhpParser\Node; use PhpParser\Node\Name; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Generic\TypeProjectionHelper; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function array_fill_keys; +use function array_filter; use function array_keys; use function array_map; use function array_merge; @@ -21,7 +26,8 @@ use function in_array; use function sprintf; -class GenericAncestorsCheck +#[AutowiredService] +final class GenericAncestorsCheck { /** @@ -31,8 +37,11 @@ public function __construct( private ReflectionProvider $reflectionProvider, private GenericObjectTypeCheck $genericObjectTypeCheck, private VarianceCheck $varianceCheck, - private bool $checkGenericClassInNonGenericObjectType, + private UnresolvableTypeHelper $unresolvableTypeHelper, + #[AutowiredParameter(ref: '%featureToggles.skipCheckGenericClasses%')] private array $skipCheckGenericClasses, + #[AutowiredParameter] + private bool $checkMissingTypehints, ) { } @@ -46,6 +55,7 @@ public function check( array $nameNodes, array $ancestorTypes, string $incompatibleTypeMessage, + string $unresolvableTypeMessage, string $noNamesMessage, string $noRelatedNameMessage, string $classNotGenericMessage, @@ -99,13 +109,31 @@ public function check( ); $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; + } + + $classReflection = $this->reflectionProvider->getClass($referencedClass); + if (!$classReflection->isTrait()) { continue; } $messages[] = RuleErrorBuilder::message(sprintf($invalidTypeMessage, $referencedClass)) - ->identifier('class.notFound') + ->identifier('generics.trait') ->build(); } @@ -131,7 +159,7 @@ public function check( } } - if ($this->checkGenericClassInNonGenericObjectType) { + if ($this->checkMissingTypehints) { foreach (array_keys($unusedNames) as $unusedName) { if (!$this->reflectionProvider->hasClass($unusedName)) { continue; @@ -145,10 +173,22 @@ 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())), + $templateTypesList, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Generics/GenericObjectTypeCheck.php b/src/Rules/Generics/GenericObjectTypeCheck.php index 9df0d06b44..0ed2f74166 100644 --- a/src/Rules/Generics/GenericObjectTypeCheck.php +++ b/src/Rules/Generics/GenericObjectTypeCheck.php @@ -2,10 +2,12 @@ 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; @@ -14,6 +16,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use PHPStan\Type\VerbosityLevel; +use function array_filter; use function array_keys; use function array_values; use function count; @@ -21,7 +24,8 @@ use function sprintf; use function strtolower; -class GenericObjectTypeCheck +#[AutowiredService] +final class GenericObjectTypeCheck { /** @@ -59,15 +63,26 @@ public function check( $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())), + $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()), @@ -75,11 +90,10 @@ public function check( $classLikeDescription, $classReflection->getDisplayName(false), $templateTypesCount, - implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())), + $templateTypesList, ))->identifier('generics.moreTypes')->build(); } - $templateTypesCount = count($templateTypes); for ($i = 0; $i < $templateTypesCount; $i++) { if (!isset($genericTypeTypes[$i])) { continue; @@ -155,15 +169,15 @@ public function check( } /** - * @return GenericObjectType[] + * @return list */ private function getGenericTypes(Type $phpDocType): array { $genericObjectTypes = []; TypeTraverser::map($phpDocType, static function (Type $type, callable $traverse) use (&$genericObjectTypes): Type { - if ($type instanceof GenericObjectType) { + if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) { $resolvedType = TemplateTypeHelper::resolveToBounds($type); - if (!$resolvedType instanceof GenericObjectType) { + if (!$resolvedType instanceof GenericObjectType && !$resolvedType instanceof GenericStaticType) { throw new ShouldNotHappenException(); } $genericObjectTypes[] = $resolvedType; diff --git a/src/Rules/Generics/InterfaceAncestorsRule.php b/src/Rules/Generics/InterfaceAncestorsRule.php index 465c2b9f36..af4f95c64e 100644 --- a/src/Rules/Generics/InterfaceAncestorsRule.php +++ b/src/Rules/Generics/InterfaceAncestorsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassNode; use PHPStan\PhpDoc\Tag\ExtendsTag; @@ -17,7 +18,8 @@ /** * @implements Rule */ -class InterfaceAncestorsRule implements Rule +#[RegisteredRule(level: 2)] +final class InterfaceAncestorsRule implements Rule { public function __construct( @@ -47,6 +49,7 @@ public function processNode(Node $node, Scope $scope): array $originalNode->extends, array_map(static fn (ExtendsTag $tag): Type => $tag->getType(), $classReflection->getExtendsTags()), sprintf('Interface %s @extends tag contains incompatible type %%s.', $escapedInterfaceName), + sprintf('Interface %s @extends tag contains unresolvable type.', $interfaceName), sprintf('Interface %s has @extends tag, but does not extend any interface.', $escapedInterfaceName), sprintf('The @extends tag of interface %s describes %%s but the interface extends: %%s', $escapedInterfaceName), 'PHPDoc tag @extends contains generic type %s but %s %s is not generic.', @@ -63,6 +66,7 @@ public function processNode(Node $node, Scope $scope): array [], array_map(static fn (ImplementsTag $tag): Type => $tag->getType(), $classReflection->getImplementsTags()), sprintf('Interface %s @implements tag contains incompatible type %%s.', $escapedInterfaceName), + sprintf('Interface %s @implements tag contains unresolvable type.', $interfaceName), sprintf('Interface %s has @implements tag, but can not implement any interface, must extend from it.', $escapedInterfaceName), '', '', diff --git a/src/Rules/Generics/InterfaceTemplateTypeRule.php b/src/Rules/Generics/InterfaceTemplateTypeRule.php index e808174a9f..e0f6197223 100644 --- a/src/Rules/Generics/InterfaceTemplateTypeRule.php +++ b/src/Rules/Generics/InterfaceTemplateTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; @@ -13,7 +14,8 @@ /** * @implements Rule */ -class InterfaceTemplateTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class InterfaceTemplateTypeRule implements Rule { public function __construct( @@ -46,6 +48,9 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template for interface %s cannot have existing type alias %%s as its name.', $escapadInterfaceName), sprintf('PHPDoc tag @template %%s for interface %s has invalid bound type %%s.', $escapadInterfaceName), sprintf('PHPDoc tag @template %%s for interface %s with bound type %%s is not supported.', $escapadInterfaceName), + sprintf('PHPDoc tag @template %%s for interface %s has invalid default type %%s.', $escapadInterfaceName), + sprintf('Default type %%s in PHPDoc tag @template %%s for interface %s is not subtype of bound type %%s.', $escapadInterfaceName), + sprintf('PHPDoc tag @template %%s for interface %s does not have a default type but follows an optional @template %%s.', $escapadInterfaceName), ); } diff --git a/src/Rules/Generics/MethodSignatureVarianceRule.php b/src/Rules/Generics/MethodSignatureVarianceRule.php index 4f99378be0..857ba59c17 100644 --- a/src/Rules/Generics/MethodSignatureVarianceRule.php +++ b/src/Rules/Generics/MethodSignatureVarianceRule.php @@ -4,16 +4,17 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassMethodNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use function sprintf; /** * @implements Rule */ -class MethodSignatureVarianceRule implements Rule +#[RegisteredRule(level: 2)] +final class MethodSignatureVarianceRule implements Rule { public function __construct(private VarianceCheck $varianceCheck) @@ -30,7 +31,7 @@ public function processNode(Node $node, Scope $scope): array $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()), 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 index 6b60d6557a..e0d21cf492 100644 --- a/src/Rules/Generics/MethodTagTemplateTypeRule.php +++ b/src/Rules/Generics/MethodTagTemplateTypeRule.php @@ -4,26 +4,19 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\Internal\SprintfHelper; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\Generic\TemplateTypeScope; -use PHPStan\Type\VerbosityLevel; -use function array_keys; -use function array_merge; -use function sprintf; /** * @implements Rule */ -class MethodTagTemplateTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class MethodTagTemplateTypeRule implements Rule { public function __construct( - private FileTypeMapper $fileTypeMapper, - private TemplateTypeCheck $templateTypeCheck, + private MethodTagTemplateTypeCheck $check, ) { } @@ -40,47 +33,12 @@ public function processNode(Node $node, Scope $scope): array return []; } - $classReflection = $node->getClassReflection(); - $className = $classReflection->getDisplayName(); - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $classReflection->getName(), - $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, - null, + return $this->check->check( + $node->getClassReflection(), + $scope, + $node->getOriginalNode(), $docComment->getText(), ); - - $messages = []; - $escapedClassName = SprintfHelper::escapeFormatString($className); - $classTemplateTypes = $classReflection->getTemplateTypeMap()->getTypes(); - - foreach ($resolvedPhpDoc->getMethodTags() as $methodName => $methodTag) { - $methodTemplateTags = $methodTag->getTemplateTags(); - $escapedMethodName = SprintfHelper::escapeFormatString($methodName); - - $messages = array_merge($messages, $this->templateTypeCheck->check( - $scope, - $node, - TemplateTypeScope::createWithMethod($className, $methodName), - $methodTemplateTags, - sprintf('PHPDoc tag @method template for method %s::%s() cannot have existing class %%s as its name.', $escapedClassName, $escapedMethodName), - sprintf('PHPDoc tag @method template for method %s::%s() cannot have existing type alias %%s as its name.', $escapedClassName, $escapedMethodName), - sprintf('PHPDoc tag @method template %%s for method %s::%s() has invalid bound type %%s.', $escapedClassName, $escapedMethodName), - sprintf('PHPDoc tag @method template %%s for method %s::%s() with bound type %%s is not supported.', $escapedClassName, $escapedMethodName), - )); - - foreach (array_keys($methodTemplateTags) as $name) { - if (!isset($classTemplateTypes[$name])) { - continue; - } - - $messages[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @method template %s for method %s::%s() shadows @template %s for class %s.', $name, $className, $methodName, $classTemplateTypes[$name]->describe(VerbosityLevel::typeOnly()), $classReflection->getDisplayName(false))) - ->identifier('methodTag.shadowTemplate') - ->build(); - } - } - - return $messages; } } 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 80f21c3de7..6e2af53a41 100644 --- a/src/Rules/Generics/MethodTemplateTypeRule.php +++ b/src/Rules/Generics/MethodTemplateTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -17,7 +18,8 @@ /** * @implements Rule */ -class MethodTemplateTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class MethodTemplateTypeRule implements Rule { public function __construct( @@ -66,6 +68,9 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template for method %s::%s() cannot have existing type alias %%s as its name.', $escapedClassName, $escapedMethodName), sprintf('PHPDoc tag @template %%s for method %s::%s() has invalid bound type %%s.', $escapedClassName, $escapedMethodName), sprintf('PHPDoc tag @template %%s for method %s::%s() with bound type %%s is not supported.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template %%s for method %s::%s() has invalid default type %%s.', $escapedClassName, $escapedMethodName), + sprintf('Default type %%s in PHPDoc tag @template %%s for method %s::%s() is not subtype of bound type %%s.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template %%s for method %s::%s() does not have a default type but follows an optional @template %%s.', $escapedClassName, $escapedMethodName), ); $classTemplateTypes = $classReflection->getTemplateTypeMap()->getTypes(); diff --git a/src/Rules/Generics/PropertyVarianceRule.php b/src/Rules/Generics/PropertyVarianceRule.php index b62c494ba5..39a4bbd78d 100644 --- a/src/Rules/Generics/PropertyVarianceRule.php +++ b/src/Rules/Generics/PropertyVarianceRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\ClassPropertyNode; use PHPStan\Rules\Rule; @@ -13,12 +14,12 @@ /** * @implements Rule */ -class PropertyVarianceRule implements Rule +#[RegisteredRule(level: 2)] +final class PropertyVarianceRule implements Rule { public function __construct( private VarianceCheck $varianceCheck, - private bool $readOnlyByPhpDoc, ) { } @@ -42,7 +43,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $variance = $node->isReadOnly() || ($this->readOnlyByPhpDoc && $node->isReadOnlyByPhpDoc()) + $variance = $node->isReadOnly() || $node->isReadOnlyByPhpDoc() ? TemplateTypeVariance::createCovariant() : TemplateTypeVariance::createInvariant(); diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index 234c4e8a71..3b7e924258 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -4,11 +4,14 @@ 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\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ArrayType; @@ -22,6 +25,7 @@ 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\ObjectShapeType; @@ -36,7 +40,8 @@ use function get_class; use function sprintf; -class TemplateTypeCheck +#[AutowiredService] +final class TemplateTypeCheck { public function __construct( @@ -44,6 +49,7 @@ public function __construct( private ClassNameCheck $classCheck, private GenericObjectTypeCheck $genericObjectTypeCheck, private TypeAliasResolver $typeAliasResolver, + #[AutowiredParameter] private bool $checkClassCaseSensitivity, ) { @@ -62,9 +68,13 @@ public function check( string $sameTemplateTypeNameAsTypeMessage, string $invalidBoundTypeMessage, string $notSupportedBoundMessage, + string $invalidDefaultTypeMessage, + string $defaultNotSubtypeOfBoundMessage, + string $requiredTypeAfterOptionalMessage, ): array { $messages = []; + $templateTagWithDefaultType = null; foreach ($templateTags as $templateTag) { $templateTagName = $scope->resolveName(new Node\Name($templateTag->getName())); if ($this->reflectionProvider->hasClass($templateTagName)) { @@ -101,7 +111,9 @@ public function check( } $classNameNodePairs = array_map(static fn (string $referencedClass): ClassNameNodePair => new ClassNameNodePair($referencedClass, $node), $boundType->getReferencedClasses()); - $messages = array_merge($messages, $this->classCheck->checkClassNames($classNameNodePairs, $this->checkClassCaseSensitivity)); + $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 ( @@ -119,6 +131,7 @@ public function check( && $boundTypeClass !== ObjectShapeType::class && $boundTypeClass !== GenericObjectType::class && $boundTypeClass !== KeyOfType::class + && $boundTypeClass !== IterableType::class && !$boundType instanceof UnionType && !$boundType instanceof IntersectionType && !$boundType instanceof TemplateType @@ -141,6 +154,67 @@ public function check( 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 c90b398fcf..34d4081bc0 100644 --- a/src/Rules/Generics/TraitTemplateTypeRule.php +++ b/src/Rules/Generics/TraitTemplateTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Rules\Rule; use PHPStan\ShouldNotHappenException; @@ -14,7 +15,8 @@ /** * @implements Rule */ -class TraitTemplateTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class TraitTemplateTypeRule implements Rule { public function __construct( @@ -60,6 +62,9 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template for trait %s cannot have existing type alias %%s as its name.', $escapedTraitName), sprintf('PHPDoc tag @template %%s for trait %s has invalid bound type %%s.', $escapedTraitName), sprintf('PHPDoc tag @template %%s for trait %s with bound type %%s is not supported.', $escapedTraitName), + sprintf('PHPDoc tag @template %%s for trait %s has invalid default type %%s.', $escapedTraitName), + sprintf('Default type %%s in PHPDoc tag @template %%s for trait %s is not subtype of bound type %%s.', $escapedTraitName), + sprintf('PHPDoc tag @template %%s for trait %s does not have a default type but follows an optional @template %%s.', $escapedTraitName), ); } diff --git a/src/Rules/Generics/UsedTraitsRule.php b/src/Rules/Generics/UsedTraitsRule.php index 5ad6141c06..9b94857f1d 100644 --- a/src/Rules/Generics/UsedTraitsRule.php +++ b/src/Rules/Generics/UsedTraitsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\PhpDoc\Tag\UsesTag; use PHPStan\Rules\Rule; @@ -18,7 +19,8 @@ /** * @implements Rule */ -class UsedTraitsRule implements Rule +#[RegisteredRule(level: 2)] +final class UsedTraitsRule implements Rule { public function __construct( @@ -64,20 +66,25 @@ public function processNode(Node $node, Scope $scope): array $description = sprintf('%s %s', $typeDescription, SprintfHelper::escapeFormatString($traitName)); } + $escapedDescription = SprintfHelper::escapeFormatString($description); + $upperCaseDescription = ucfirst($description); + $escapedUpperCaseDescription = SprintfHelper::escapeFormatString($upperCaseDescription); + return $this->genericAncestorsCheck->check( $node->traits, array_map(static fn (UsesTag $tag): Type => $tag->getType(), $useTags), - sprintf('%s @use tag contains incompatible type %%s.', ucfirst($description)), - sprintf('%s has @use tag, but does not use any trait.', ucfirst($description)), - sprintf('The @use tag of %s describes %%s but the %s uses %%s.', $description, $typeDescription), + sprintf('%s @use tag contains incompatible type %%s.', $escapedUpperCaseDescription), + sprintf('%s @use tag contains unresolvable type.', $upperCaseDescription), + sprintf('%s has @use tag, but does not use any trait.', $upperCaseDescription), + sprintf('The @use tag of %s describes %%s but the %s uses %%s.', $escapedDescription, $typeDescription), 'PHPDoc tag @use contains generic type %s but %s %s is not generic.', 'Generic type %s in PHPDoc tag @use does not specify all template types of %s %s: %s', 'Generic type %s in PHPDoc tag @use specifies %d template types, but %s %s supports only %d: %s', 'Type %s in generic type %s in PHPDoc tag @use is not subtype of template type %s of %s %s.', 'Call-site variance annotation of %s in generic type %s in PHPDoc tag @use is not allowed.', 'PHPDoc tag @use has invalid type %s.', - sprintf('%s uses generic trait %%s but does not specify its types: %%s', ucfirst($description)), - sprintf('in used type %%s of %s', $description), + sprintf('%s uses generic trait %%s but does not specify its types: %%s', $escapedUpperCaseDescription), + sprintf('in used type %%s of %s', $escapedDescription), ); } diff --git a/src/Rules/Generics/VarianceCheck.php b/src/Rules/Generics/VarianceCheck.php index 7447c13cce..cd8d7192bf 100644 --- a/src/Rules/Generics/VarianceCheck.php +++ b/src/Rules/Generics/VarianceCheck.php @@ -2,7 +2,8 @@ namespace PHPStan\Rules\Generics; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Generic\TemplateType; @@ -10,22 +11,16 @@ use PHPStan\Type\Type; use function sprintf; -class VarianceCheck +#[AutowiredService] +final class VarianceCheck { - public function __construct( - private bool $checkParamOutVariance, - private bool $strictStaticVariance, - ) - { - } - /** * @param 'function'|'method' $identifier * @return list */ public function checkParametersAcceptor( - ParametersAcceptorWithPhpDocs $parametersAcceptor, + ExtendedParametersAcceptor $parametersAcceptor, string $parameterTypeMessage, string $parameterOutTypeMessage, string $returnTypeMessage, @@ -57,9 +52,7 @@ public function checkParametersAcceptor( } $covariant = TemplateTypeVariance::createCovariant(); - $parameterVariance = $isStatic && !$this->strictStaticVariance - ? TemplateTypeVariance::createStatic() - : TemplateTypeVariance::createContravariant(); + $parameterVariance = TemplateTypeVariance::createContravariant(); foreach ($parametersAcceptor->getParameters() as $parameterReflection) { $type = $parameterReflection->getType(); @@ -68,10 +61,6 @@ public function checkParametersAcceptor( $errors[] = $error; } - if (!$this->checkParamOutVariance) { - continue; - } - $paramOutType = $parameterReflection->getOutType(); if ($paramOutType === null) { continue; diff --git a/src/Rules/Ignore/IgnoreParseErrorRule.php b/src/Rules/Ignore/IgnoreParseErrorRule.php index 330ac5a8d1..863542c96a 100644 --- a/src/Rules/Ignore/IgnoreParseErrorRule.php +++ b/src/Rules/Ignore/IgnoreParseErrorRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\FileNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -13,7 +14,8 @@ /** * @implements Rule */ -class IgnoreParseErrorRule implements Rule +#[RegisteredRule(level: 0)] +final class IgnoreParseErrorRule implements Rule { public function getNodeType(): string 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 a660be4e61..28b7780543 100644 --- a/src/Rules/IssetCheck.php +++ b/src/Rules/IssetCheck.php @@ -5,27 +5,32 @@ 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; use function is_string; use function sprintf; +use function str_starts_with; /** * @phpstan-type ErrorIdentifier = 'empty'|'isset'|'nullCoalesce' */ -class IssetCheck +#[AutowiredService] +final class IssetCheck { public function __construct( private PropertyDescriptor $propertyDescriptor, private PropertyReflectionFinder $propertyReflectionFinder, + #[AutowiredParameter] private bool $checkAdvancedIsset, + #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, - private bool $strictUnnecessaryNullsafePropertyFetch, ) { } @@ -144,8 +149,38 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, str return null; } - $nativeType = $propertyReflection->getNativeType(); - if (!$nativeType instanceof MixedType) { + if ($propertyReflection->hasNativeType() && !$propertyReflection->isVirtual()->yes()) { + if ( + $expr instanceof Node\Expr\PropertyFetch + && $expr->name instanceof Node\Identifier + && $expr->var instanceof Expr\Variable + && $expr->var->name === 'this' + && $scope->hasExpressionType(new PropertyInitializationExpr($propertyReflection->getName()))->yes() + ) { + return $this->generateError( + $propertyReflection->getNativeType(), + sprintf( + '%s %s', + $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $expr), + $operatorDescription, + ), + static function (Type $type) use ($typeMessageCallback): ?string { + $originalMessage = $typeMessageCallback($type); + if ($originalMessage === null) { + return null; + } + + if (str_starts_with($originalMessage, 'is not')) { + return sprintf('%s nor uninitialized', $originalMessage); + } + + return sprintf('%s and initialized', $originalMessage); + }, + $identifier, + 'initializedProperty', + ); + } + if (!$scope->hasExpressionType($expr)->yes()) { if ($expr instanceof Node\Expr\PropertyFetch) { return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); @@ -217,10 +252,6 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, str } if ($expr instanceof Expr\NullsafePropertyFetch) { - if (!$this->strictUnnecessaryNullsafePropertyFetch) { - return null; - } - 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') @@ -287,7 +318,7 @@ private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescri /** * @param callable(Type): ?string $typeMessageCallback * @param ErrorIdentifier $identifier - * @param 'variable'|'offset'|'property'|'expr' $identifierSecondPart + * @param 'variable'|'offset'|'property'|'expr'|'initializedProperty' $identifierSecondPart */ private function generateError(Type $type, string $message, callable $typeMessageCallback, string $identifier, string $identifierSecondPart): ?IdentifierRuleError { diff --git a/src/Rules/Keywords/ContinueBreakInLoopRule.php b/src/Rules/Keywords/ContinueBreakInLoopRule.php index d97e3fd119..e07d63a5b7 100644 --- a/src/Rules/Keywords/ContinueBreakInLoopRule.php +++ b/src/Rules/Keywords/ContinueBreakInLoopRule.php @@ -5,16 +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 @@ -28,7 +31,7 @@ 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; @@ -39,11 +42,7 @@ public function processNode(Node $node, Scope $scope): array if ($parentStmtType === Stmt\Case_::class) { continue; } - if ( - $parentStmtType === Stmt\Function_::class - || $parentStmtType === Stmt\ClassMethod::class - || $parentStmtType === Node\Expr\Closure::class - ) { + if ($parentStmtType === Node\Expr\Closure::class) { return [ RuleErrorBuilder::message(sprintf( 'Keyword %s used outside of a loop or a switch statement.', @@ -54,13 +53,13 @@ public function processNode(Node $node, Scope $scope): array ->build(), ]; } - if ( - $parentStmtType === Stmt\For_::class - || $parentStmtType === Stmt\Foreach_::class - || $parentStmtType === Stmt\Do_::class - || $parentStmtType === Stmt\While_::class - || $parentStmtType === Stmt\Switch_::class - ) { + if (in_array($parentStmtType, [ + Stmt\For_::class, + Stmt\Foreach_::class, + Stmt\Do_::class, + Stmt\While_::class, + Stmt\Switch_::class, + ], true)) { $value--; } if ($value === 0) { diff --git a/src/Rules/Keywords/DeclareStrictTypesRule.php b/src/Rules/Keywords/DeclareStrictTypesRule.php index 3878c1c77c..52ad44c098 100644 --- a/src/Rules/Keywords/DeclareStrictTypesRule.php +++ b/src/Rules/Keywords/DeclareStrictTypesRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Stmt; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Parser\DeclarePositionVisitor; use PHPStan\Rules\Rule; @@ -15,7 +16,8 @@ /** * @implements Rule */ -class DeclareStrictTypesRule implements Rule +#[RegisteredRule(level: 0)] +final class DeclareStrictTypesRule implements Rule { public function __construct( @@ -40,7 +42,7 @@ public function processNode(Node $node, Scope $scope): array } if ( - !$declare->value instanceof Node\Scalar\LNumber + !$declare->value instanceof Node\Scalar\Int_ || !in_array($declare->value->value, [0, 1], true) ) { return [ 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 index a9a1a728ec..4a55509947 100644 --- a/src/Rules/LazyRegistry.php +++ b/src/Rules/LazyRegistry.php @@ -3,11 +3,13 @@ namespace PHPStan\Rules; use PhpParser\Node; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; use function class_implements; use function class_parents; -class LazyRegistry implements Registry +#[AutowiredService(name: 'registry', as: Registry::class)] +final class LazyRegistry implements Registry { public const RULE_TAG = 'phpstan.rules.rule'; @@ -24,10 +26,8 @@ public function __construct(private Container $container) /** * @template TNodeType of Node - * @phpstan-param class-string $nodeType - * @param Node $nodeType - * @phpstan-return array> - * @return Rule[] + * @param class-string $nodeType + * @return array> */ public function getRules(string $nodeType): array { @@ -46,8 +46,7 @@ public function getRules(string $nodeType): array } /** - * @phpstan-var array> $selectedRules - * @var Rule[] $selectedRules + * @var array> $selectedRules */ $selectedRules = $this->cache[$nodeType]; diff --git a/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php b/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php index d6d7b603c8..3a039e6f2a 100644 --- a/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php +++ b/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php @@ -4,15 +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 @@ -29,6 +32,18 @@ public function processNode(Node $node, Scope $scope): array $class = $scope->getClassReflection(); 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( diff --git a/src/Rules/Methods/AbstractPrivateMethodRule.php b/src/Rules/Methods/AbstractPrivateMethodRule.php index 060bb2a27a..4884bbd956 100644 --- a/src/Rules/Methods/AbstractPrivateMethodRule.php +++ b/src/Rules/Methods/AbstractPrivateMethodRule.php @@ -4,13 +4,15 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassMethodNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; /** @implements Rule */ -class AbstractPrivateMethodRule implements Rule +#[RegisteredRule(level: 0)] +final class AbstractPrivateMethodRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Methods/AlwaysUsedMethodExtension.php b/src/Rules/Methods/AlwaysUsedMethodExtension.php index cfccf5b972..f52b6a9c2b 100644 --- a/src/Rules/Methods/AlwaysUsedMethodExtension.php +++ b/src/Rules/Methods/AlwaysUsedMethodExtension.php @@ -2,7 +2,7 @@ namespace PHPStan\Rules\Methods; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; /** * This is the extension interface to implement if you want to describe an always-used class method. @@ -22,6 +22,6 @@ interface AlwaysUsedMethodExtension { - public function isAlwaysUsed(MethodReflection $methodReflection): bool; + public function isAlwaysUsed(ExtendedMethodReflection $methodReflection): bool; } diff --git a/src/Rules/Methods/CallMethodsRule.php b/src/Rules/Methods/CallMethodsRule.php index 388dcb3208..1f042288f0 100644 --- a/src/Rules/Methods/CallMethodsRule.php +++ b/src/Rules/Methods/CallMethodsRule.php @@ -3,18 +3,23 @@ namespace PHPStan\Rules\Methods; use PhpParser\Node; +use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use function array_merge; /** * @implements Rule */ -class CallMethodsRule implements Rule +#[RegisteredRule(level: 0)] +final class CallMethodsRule implements Rule { public function __construct( @@ -31,13 +36,35 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->name instanceof Node\Identifier) { - return []; + $errors = []; + if ($node->name instanceof Node\Identifier) { + $methodNameScopes = [$node->name->name => $scope]; + } else { + $nameType = $scope->getType($node->name); + $methodNameScopes = []; + foreach ($nameType->getConstantStrings() as $constantString) { + $name = $constantString->getValue(); + $methodNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name))); + } } - $methodName = $node->name->name; + foreach ($methodNameScopes as $methodName => $methodScope) { + $errors = array_merge($errors, $this->processSingleMethodCall( + $methodScope, + $node, + (string) $methodName, // @phpstan-ignore cast.useless + )); + } + + return $errors; + } - [$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodName, $node->var); + /** + * @return list + */ + private function processSingleMethodCall(Scope $scope, MethodCall $node, string $methodName): array + { + [$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodName, $node->var, $node->name); if ($methodReflection === null) { return $errors; } @@ -55,23 +82,23 @@ public function processNode(Node $node, Scope $scope): array $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.', - 'Parameter %s of method ' . $messagesMethodName . ' contains unresolvable type.', - ], 'method', + $methodReflection->acceptsNamedArguments(), + 'Method ' . $messagesMethodName . ' invoked with %d parameter, %d required.', + 'Method ' . $messagesMethodName . ' invoked with %d parameters, %d required.', + 'Method ' . $messagesMethodName . ' invoked with %d parameter, at least %d required.', + 'Method ' . $messagesMethodName . ' invoked with %d parameters, at least %d required.', + 'Method ' . $messagesMethodName . ' invoked with %d parameter, %d-%d required.', + 'Method ' . $messagesMethodName . ' invoked with %d parameters, %d-%d required.', + '%s of method ' . $messagesMethodName . ' expects %s, %s given.', + 'Result of method ' . $messagesMethodName . ' (void) is used.', + '%s of method ' . $messagesMethodName . ' is passed by reference, so it expects variables only.', + 'Unable to resolve the template type %s in call to method ' . $messagesMethodName, + 'Missing parameter $%s in call to method ' . $messagesMethodName . '.', + 'Unknown parameter $%s in call to method ' . $messagesMethodName . '.', + 'Return type of call to method ' . $messagesMethodName . ' contains unresolvable type.', + '%s of method ' . $messagesMethodName . ' contains unresolvable type.', + 'Method ' . $messagesMethodName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', )); } diff --git a/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php b/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php index 0a37e48657..327b2ba473 100644 --- a/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php +++ b/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php @@ -6,6 +6,7 @@ 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; @@ -13,7 +14,8 @@ /** * @implements Rule */ -class CallPrivateMethodThroughStaticRule implements Rule +#[RegisteredRule(level: 2)] +final class CallPrivateMethodThroughStaticRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index b1f2f013c2..c9430abd81 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -3,11 +3,15 @@ namespace PHPStan\Rules\Methods; use PhpParser\Node; +use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use function array_merge; use function sprintf; @@ -15,7 +19,8 @@ /** * @implements Rule */ -class CallStaticMethodsRule implements Rule +#[RegisteredRule(level: 0)] +final class CallStaticMethodsRule implements Rule { public function __construct( @@ -32,11 +37,34 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->name instanceof Node\Identifier) { - return []; + $errors = []; + if ($node->name instanceof Node\Identifier) { + $methodNameScopes = [$node->name->name => $scope]; + } else { + $nameType = $scope->getType($node->name); + $methodNameScopes = []; + foreach ($nameType->getConstantStrings() as $constantString) { + $name = $constantString->getValue(); + $methodNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name))); + } } - $methodName = $node->name->name; + foreach ($methodNameScopes as $methodName => $methodScope) { + $errors = array_merge($errors, $this->processSingleMethodCall( + $methodScope, + $node, + (string) $methodName, // @phpstan-ignore cast.useless + )); + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleMethodCall(Scope $scope, StaticCall $node, string $methodName): array + { [$errors, $method] = $this->methodCallCheck->check($scope, $methodName, $node->class); if ($method === null) { return $errors; @@ -63,23 +91,23 @@ public function processNode(Node $node, Scope $scope): array $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.', - 'Parameter %s of ' . $lowercasedMethodName . ' contains unresolvable type.', - ], 'staticMethod', + $method->acceptsNamedArguments(), + $displayMethodName . ' invoked with %d parameter, %d required.', + $displayMethodName . ' invoked with %d parameters, %d required.', + $displayMethodName . ' invoked with %d parameter, at least %d required.', + $displayMethodName . ' invoked with %d parameters, at least %d required.', + $displayMethodName . ' invoked with %d parameter, %d-%d required.', + $displayMethodName . ' invoked with %d parameters, %d-%d required.', + '%s of ' . $lowercasedMethodName . ' expects %s, %s given.', + 'Result of ' . $lowercasedMethodName . ' (void) is used.', + '%s of ' . $lowercasedMethodName . ' is passed by reference, so it expects variables only.', + 'Unable to resolve the template type %s in call to method ' . $lowercasedMethodName, + 'Missing parameter $%s in call to ' . $lowercasedMethodName . '.', + 'Unknown parameter $%s in call to ' . $lowercasedMethodName . '.', + 'Return type of call to ' . $lowercasedMethodName . ' contains unresolvable type.', + '%s of ' . $lowercasedMethodName . ' contains unresolvable type.', + $displayMethodName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', )); return $errors; diff --git a/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php b/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php index da361f018a..96f3072f1b 100644 --- a/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php +++ b/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php @@ -4,6 +4,8 @@ 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; @@ -11,30 +13,30 @@ use function sprintf; /** - * @implements Rule + * @implements Rule */ -class CallToConstructorStatementWithoutSideEffectsRule implements Rule +#[RegisteredRule(level: 4)] +final class CallToConstructorStatementWithoutSideEffectsRule implements Rule { public function __construct( private ReflectionProvider $reflectionProvider, - private bool $reportNoConstructor, ) { } 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,40 +48,27 @@ public function processNode(Node $node, Scope $scope): array $classReflection = $this->reflectionProvider->getClass($className); if (!$classReflection->hasConstructor()) { - if ($this->reportNoConstructor) { - return [ - RuleErrorBuilder::message(sprintf( - 'Call to new %s() on a separate line has no effect.', - $classReflection->getDisplayName(), - ))->identifier('new.resultUnused')->build(), - ]; - } - - return []; - } - - $constructor = $classReflection->getConstructor(); - if ($constructor->hasSideEffects()->no()) { - $throwsType = $constructor->getThrowType(); - if ($throwsType !== null && !$throwsType->isVoid()->yes()) { - 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(), ))->identifier('new.resultUnused')->build(), ]; } - return []; + $constructor = $classReflection->getConstructor(); + $methodResult = $scope->getType($instantiation); + if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Call to %s::%s() on a separate line has no effect.', + $classReflection->getDisplayName(), + $constructor->getName(), + ))->identifier('new.resultUnused')->build(), + ]; } } diff --git a/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php b/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php index abeaff4110..9b6adbdd14 100644 --- a/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php +++ b/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php @@ -5,6 +5,8 @@ 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; @@ -14,9 +16,10 @@ use function sprintf; /** - * @implements Rule + * @implements Rule */ -class CallToMethodStatementWithoutSideEffectsRule implements Rule +#[RegisteredRule(level: 4)] +final class CallToMethodStatementWithoutSideEffectsRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) @@ -25,18 +28,18 @@ public function __construct(private RuleLevelHelper $ruleLevelHelper) public function getNodeType(): string { - return Node\Stmt\Expression::class; + return NoopExpressionNode::class; } public function processNode(Node $node, Scope $scope): array { - if ($node->expr instanceof Node\Expr\NullsafeMethodCall) { - $scope = $scope->filterByTruthyValue(new Node\Expr\BinaryOp\NotIdentical($node->expr->var, new Node\Expr\ConstFetch(new Node\Name('null')))); - } elseif (!$node->expr instanceof Node\Expr\MethodCall) { + $methodCall = $node->getOriginalExpr(); + if ($methodCall instanceof Node\Expr\NullsafeMethodCall) { + $scope = $scope->filterByTruthyValue(new Node\Expr\BinaryOp\NotIdentical($methodCall->var, new Node\Expr\ConstFetch(new Node\Name('null')))); + } elseif (!$methodCall instanceof Node\Expr\MethodCall) { return []; } - $methodCall = $node->expr; if (!$methodCall->name instanceof Node\Identifier) { return []; } @@ -60,31 +63,21 @@ public function processNode(Node $node, Scope $scope): array return []; } - $method = $calledOnType->getMethod($methodName, $scope); - if ($method->hasSideEffects()->no() || $node->expr->isFirstClassCallable()) { - if (!$node->expr->isFirstClassCallable()) { - $throwsType = $method->getThrowType(); - if ($throwsType !== null && !$throwsType->isVoid()->yes()) { - 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(), - ))->identifier('method.resultUnused')->build(), - ]; + $methodResult = $scope->getType($methodCall); + if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { + return []; } - return []; + $method = $calledOnType->getMethod($methodName, $scope); + + return [ + RuleErrorBuilder::message(sprintf( + 'Call to %s %s::%s() on a separate line has no effect.', + $method->isStatic() ? 'static method' : 'method', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('method.resultUnused')->build(), + ]; } } diff --git a/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php b/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php index 9db2493f65..31dc4dd096 100644 --- a/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php +++ b/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php @@ -5,6 +5,8 @@ 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; @@ -17,9 +19,10 @@ use function strtolower; /** - * @implements Rule + * @implements Rule */ -class CallToStaticMethodStatementWithoutSideEffectsRule implements Rule +#[RegisteredRule(level: 4)] +final class CallToStaticMethodStatementWithoutSideEffectsRule implements Rule { public function __construct( @@ -31,16 +34,16 @@ public function __construct( public function getNodeType(): string { - return Node\Stmt\Expression::class; + return NoopExpressionNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!$node->expr instanceof Node\Expr\StaticCall) { + $staticCall = $node->getOriginalExpr(); + if (!$staticCall instanceof Node\Expr\StaticCall) { return []; } - $staticCall = $node->expr; if (!$staticCall->name instanceof Node\Identifier) { return []; } @@ -84,30 +87,19 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($method->hasSideEffects()->no() || $node->expr->isFirstClassCallable()) { - if (!$node->expr->isFirstClassCallable()) { - $throwsType = $method->getThrowType(); - if ($throwsType !== null && !$throwsType->isVoid()->yes()) { - return []; - } - } - - $methodResult = $scope->getType($staticCall); - if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { - return []; - } - - return [ - RuleErrorBuilder::message(sprintf( - 'Call to %s %s::%s() on a separate line has no effect.', - $method->isStatic() ? 'static method' : 'method', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - ))->identifier('staticMethod.resultUnused')->build(), - ]; + $methodResult = $scope->getType($staticCall); + if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { + return []; } - return []; + return [ + RuleErrorBuilder::message(sprintf( + 'Call to %s %s::%s() on a separate line has no effect.', + $method->isStatic() ? 'static method' : 'method', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('staticMethod.resultUnused')->build(), + ]; } } diff --git a/src/Rules/Methods/ConsistentConstructorRule.php b/src/Rules/Methods/ConsistentConstructorRule.php index e32d6fa551..534e629acd 100644 --- a/src/Rules/Methods/ConsistentConstructorRule.php +++ b/src/Rules/Methods/ConsistentConstructorRule.php @@ -4,17 +4,21 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassMethodNode; use PHPStan\Reflection\Dummy\DummyConstructorReflection; use PHPStan\Rules\Rule; +use function array_merge; use function strtolower; /** @implements Rule */ -class ConsistentConstructorRule implements Rule +#[RegisteredRule(level: 0)] +final class ConsistentConstructorRule implements Rule { public function __construct( private MethodParameterComparisonHelper $methodParameterComparisonHelper, + private MethodVisibilityComparisonHelper $methodVisibilityComparisonHelper, ) { } @@ -47,7 +51,10 @@ public function processNode(Node $node, Scope $scope): array return []; } - return $this->methodParameterComparisonHelper->compare($parentConstructor, $parentConstructor->getDeclaringClass(), $method, true); + 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 index c93634b94e..adfaff6c0c 100644 --- a/src/Rules/Methods/ConstructorReturnTypeRule.php +++ b/src/Rules/Methods/ConstructorReturnTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassMethodNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -12,7 +13,8 @@ /** * @implements Rule */ -class ConstructorReturnTypeRule implements Rule +#[RegisteredRule(level: 0)] +final class ConstructorReturnTypeRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Methods/DirectAlwaysUsedMethodExtensionProvider.php b/src/Rules/Methods/DirectAlwaysUsedMethodExtensionProvider.php index b51cf7763a..3012ef21f2 100644 --- a/src/Rules/Methods/DirectAlwaysUsedMethodExtensionProvider.php +++ b/src/Rules/Methods/DirectAlwaysUsedMethodExtensionProvider.php @@ -2,7 +2,7 @@ namespace PHPStan\Rules\Methods; -class DirectAlwaysUsedMethodExtensionProvider implements AlwaysUsedMethodExtensionProvider +final class DirectAlwaysUsedMethodExtensionProvider implements AlwaysUsedMethodExtensionProvider { /** diff --git a/src/Rules/Methods/ExistingClassesInTypehintsRule.php b/src/Rules/Methods/ExistingClassesInTypehintsRule.php index 524f509395..8fb0686000 100644 --- a/src/Rules/Methods/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Methods/ExistingClassesInTypehintsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassMethodNode; use PHPStan\Rules\FunctionDefinitionCheck; @@ -13,7 +14,8 @@ /** * @implements Rule */ -class ExistingClassesInTypehintsRule implements Rule +#[RegisteredRule(level: 0)] +final class ExistingClassesInTypehintsRule implements Rule { public function __construct(private FunctionDefinitionCheck $check) @@ -32,6 +34,7 @@ public function processNode(Node $node, Scope $scope): array $methodName = SprintfHelper::escapeFormatString($methodReflection->getName()); return $this->check->checkClassMethod( + $scope, $methodReflection, $node->getOriginalNode(), sprintf( @@ -56,6 +59,11 @@ public function processNode(Node $node, Scope $scope): array $className, $methodName, ), + sprintf( + 'Method %s::%s() has invalid @phpstan-self-out type %%s.', + $className, + $methodName, + ), ); } diff --git a/src/Rules/Methods/FinalPrivateMethodRule.php b/src/Rules/Methods/FinalPrivateMethodRule.php index b0934bb0c4..20ea6ab6fa 100644 --- a/src/Rules/Methods/FinalPrivateMethodRule.php +++ b/src/Rules/Methods/FinalPrivateMethodRule.php @@ -4,22 +4,17 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassMethodNode; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; /** @implements Rule */ -class FinalPrivateMethodRule implements Rule +#[RegisteredRule(level: 0)] +final class FinalPrivateMethodRule implements Rule { - public function __construct( - private PhpVersion $phpVersion, - ) - { - } - public function getNodeType(): string { return InClassMethodNode::class; @@ -28,7 +23,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $method = $node->getMethodReflection(); - if (!$this->phpVersion->producesWarningForFinalPrivateMethods()) { + if ($scope->getPhpVersion()->producesWarningForFinalPrivateMethods()->no()) { return []; } diff --git a/src/Rules/Methods/IllegalConstructorMethodCallRule.php b/src/Rules/Methods/IllegalConstructorMethodCallRule.php deleted file mode 100644 index 3305529134..0000000000 --- a/src/Rules/Methods/IllegalConstructorMethodCallRule.php +++ /dev/null @@ -1,34 +0,0 @@ - - */ -class IllegalConstructorMethodCallRule implements Rule -{ - - public function getNodeType(): string - { - return Node\Expr\MethodCall::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== '__construct') { - return []; - } - - return [ - RuleErrorBuilder::message('Call to __construct() on an existing object is not allowed.') - ->identifier('constructor.call') - ->build(), - ]; - } - -} diff --git a/src/Rules/Methods/IllegalConstructorStaticCallRule.php b/src/Rules/Methods/IllegalConstructorStaticCallRule.php deleted file mode 100644 index 6e839f54ca..0000000000 --- a/src/Rules/Methods/IllegalConstructorStaticCallRule.php +++ /dev/null @@ -1,92 +0,0 @@ - - */ -class IllegalConstructorStaticCallRule implements Rule -{ - - public function getNodeType(): string - { - return Node\Expr\StaticCall::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== '__construct') { - return []; - } - - if ($this->isCollectCallingConstructor($node, $scope)) { - return []; - } - - return [ - RuleErrorBuilder::message('Static call to __construct() is only allowed on a parent class in the constructor.') - ->identifier('constructor.call') - ->build(), - ]; - } - - private function isCollectCallingConstructor(Node\Expr\StaticCall $node, Scope $scope): bool - { - // __construct should be called from inside constructor - if ($scope->getFunction() === null) { - return false; - } - - if ($scope->getFunction()->getName() !== '__construct') { - if (!$this->isInRenamedTraitConstructor($scope)) { - return false; - } - } - - if (!$scope->isInClass()) { - return false; - } - - if (!$node->class instanceof Node\Name) { - return false; - } - - $parentClasses = array_map(static fn (string $name) => strtolower($name), $scope->getClassReflection()->getParentClassesNames()); - - return in_array(strtolower($scope->resolveName($node->class)), $parentClasses, true); - } - - private function isInRenamedTraitConstructor(Scope $scope): bool - { - if (!$scope->isInClass()) { - return false; - } - - if (!$scope->isInTrait()) { - return false; - } - - if ($scope->getFunction() === null) { - return false; - } - - $traitAliases = $scope->getClassReflection()->getNativeReflection()->getTraitAliases(); - $functionName = $scope->getFunction()->getName(); - if (!array_key_exists($functionName, $traitAliases)) { - return false; - } - - return $traitAliases[$functionName] === sprintf('%s::%s', $scope->getTraitReflection()->getName(), '__construct'); - } - -} diff --git a/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php b/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php index 92aa955a0e..ff239d9a2d 100644 --- a/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php +++ b/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php @@ -4,8 +4,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassMethodNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; @@ -17,7 +17,8 @@ /** * @implements Rule */ -class IncompatibleDefaultParameterTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class IncompatibleDefaultParameterTypeRule implements Rule { public function getNodeType(): string @@ -28,8 +29,6 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $method = $node->getMethodReflection(); - $parameters = ParametersAcceptorSelector::selectSingle($method->getVariants()); - $errors = []; foreach ($node->getOriginalNode()->getParams() as $paramI => $param) { if ($param->default === null) { @@ -43,11 +42,11 @@ public function processNode(Node $node, Scope $scope): array } $defaultValueType = $scope->getType($param->default); - $parameter = $parameters->getParameters()[$paramI]; + $parameter = $method->getParameters()[$paramI]; $parameterType = $parameter->getType(); $parameterType = TemplateTypeHelper::resolveToBounds($parameterType); - $accepts = $parameterType->acceptsWithReason($defaultValueType, true); + $accepts = $parameterType->accepts($defaultValueType, true); if ($accepts->yes()) { continue; } diff --git a/src/Rules/Methods/LazyAlwaysUsedMethodExtensionProvider.php b/src/Rules/Methods/LazyAlwaysUsedMethodExtensionProvider.php index cbd397ee94..1a2b49ee38 100644 --- a/src/Rules/Methods/LazyAlwaysUsedMethodExtensionProvider.php +++ b/src/Rules/Methods/LazyAlwaysUsedMethodExtensionProvider.php @@ -2,9 +2,11 @@ namespace PHPStan\Rules\Methods; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; -class LazyAlwaysUsedMethodExtensionProvider implements AlwaysUsedMethodExtensionProvider +#[AutowiredService] +final class LazyAlwaysUsedMethodExtensionProvider implements AlwaysUsedMethodExtensionProvider { /** @var AlwaysUsedMethodExtension[]|null */ diff --git a/src/Rules/Methods/MethodAttributesRule.php b/src/Rules/Methods/MethodAttributesRule.php index ae220df96e..56bb6016a1 100644 --- a/src/Rules/Methods/MethodAttributesRule.php +++ b/src/Rules/Methods/MethodAttributesRule.php @@ -5,13 +5,16 @@ 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 { public function __construct(private AttributesCheck $attributesCheck) @@ -20,14 +23,14 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Stmt\ClassMethod::class; + return InClassMethodNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, + $node->getOriginalNode()->attrGroups, Attribute::TARGET_METHOD, 'method', ); diff --git a/src/Rules/Methods/MethodCallCheck.php b/src/Rules/Methods/MethodCallCheck.php index 57d0c165e6..3be2032165 100644 --- a/src/Rules/Methods/MethodCallCheck.php +++ b/src/Rules/Methods/MethodCallCheck.php @@ -2,9 +2,14 @@ namespace PHPStan\Rules\Methods; +use PhpParser\Node\Arg; use PhpParser\Node\Expr; +use PhpParser\Node\Identifier; +use PhpParser\Node\Name\FullyQualified; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ReflectionProvider; @@ -19,13 +24,16 @@ use function sprintf; use function strtolower; -class MethodCallCheck +#[AutowiredService] +final class MethodCallCheck { public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, + #[AutowiredParameter] private bool $checkFunctionNameCase, + #[AutowiredParameter] private bool $reportMagicMethods, ) { @@ -38,6 +46,7 @@ public function check( Scope $scope, string $methodName, Expr $var, + Identifier|Expr $astName, ): array { $typeResult = $this->ruleLevelHelper->findTypeToCheck( @@ -56,7 +65,7 @@ public function check( if ($type instanceof StaticType) { $typeForDescribe = $type->getStaticObjectType(); } - if (!$type->canCallMethods()->yes() || $type->isClassStringType()->yes()) { + if (!$type->canCallMethods()->yes() || $type->isClassString()->yes()) { return [ [ RuleErrorBuilder::message(sprintf( @@ -107,6 +116,17 @@ public function check( } } + if ($astName instanceof Expr) { + $methodExistsExpr = new Expr\FuncCall(new FullyQualified('method_exists'), [ + new Arg($var), + new Arg($astName), + ]); + + if ($scope->getType($methodExistsExpr)->isTrue()->yes()) { + return [[], null]; + } + } + return [ [ RuleErrorBuilder::message(sprintf( diff --git a/src/Rules/Methods/MethodCallableRule.php b/src/Rules/Methods/MethodCallableRule.php index 15027c8c2c..395089046f 100644 --- a/src/Rules/Methods/MethodCallableRule.php +++ b/src/Rules/Methods/MethodCallableRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\MethodCallableNode; use PHPStan\Php\PhpVersion; @@ -14,7 +15,8 @@ /** * @implements Rule */ -class MethodCallableRule implements Rule +#[RegisteredRule(level: 0)] +final class MethodCallableRule implements Rule { public function __construct(private MethodCallCheck $methodCallCheck, private PhpVersion $phpVersion) @@ -44,7 +46,7 @@ public function processNode(Node $node, Scope $scope): array $methodNameName = $methodName->toString(); - [$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodNameName, $node->getVar()); + [$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodNameName, $node->getVar(), $node->getName()); if ($methodReflection === null) { return $errors; } diff --git a/src/Rules/Methods/MethodParameterComparisonHelper.php b/src/Rules/Methods/MethodParameterComparisonHelper.php index 357d3cf9b3..a52dbe86f5 100644 --- a/src/Rules/Methods/MethodParameterComparisonHelper.php +++ b/src/Rules/Methods/MethodParameterComparisonHelper.php @@ -2,14 +2,15 @@ namespace PHPStan\Rules\Methods; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ArrayType; +use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\IterableType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; @@ -21,24 +22,24 @@ use function count; use function sprintf; -class MethodParameterComparisonHelper +#[AutowiredService] +final class MethodParameterComparisonHelper { - public function __construct(private PhpVersion $phpVersion, private bool $genericPrototypeMessage) + public function __construct(private PhpVersion $phpVersion) { } /** * @return list */ - public function compare(ExtendedMethodReflection $prototype, ClassReflection $prototypeDeclaringClass, PhpMethodFromParserNodeReflection $method, bool $ignorable = false): array + public function compare(ExtendedMethodReflection $prototype, ClassReflection $prototypeDeclaringClass, PhpMethodFromParserNodeReflection $method, bool $ignorable): array { /** @var list $messages */ $messages = []; $prototypeVariant = $prototype->getVariants()[0]; - $methodVariant = ParametersAcceptorSelector::selectSingle($method->getVariants()); - $methodParameters = $methodVariant->getParameters(); + $methodParameters = $method->getParameters(); $prototypeAfterVariadic = false; foreach ($prototypeVariant->getParameters() as $i => $prototypeParameter) { @@ -47,7 +48,7 @@ public function compare(ExtendedMethodReflection $prototype, ClassReflection $pr 'Method %s::%s() overrides method %s::%s() but misses parameter #%d $%s.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), $i + 1, $prototypeParameter->getName(), @@ -73,7 +74,7 @@ public function compare(ExtendedMethodReflection $prototype, ClassReflection $pr $method->getName(), $i + 1, $prototypeParameter->getName(), - $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), ))->identifier('parameter.byRef'); @@ -92,7 +93,7 @@ public function compare(ExtendedMethodReflection $prototype, ClassReflection $pr $method->getName(), $i + 1, $prototypeParameter->getName(), - $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), ))->identifier('parameter.notByRef'); @@ -133,7 +134,7 @@ public function compare(ExtendedMethodReflection $prototype, ClassReflection $pr $method->getName(), $i + 1, $prototypeParameter->getName(), - $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), ))->identifier('parameter.notVariadic'); @@ -178,7 +179,7 @@ public function compare(ExtendedMethodReflection $prototype, ClassReflection $pr $i + $j + 1, $remainingPrototypeParameter->getName(), $remainingPrototypeParameter->getNativeType()->describe(VerbosityLevel::typeOnly()), - $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), ))->identifier('method.childParameterType'); @@ -198,7 +199,7 @@ public function compare(ExtendedMethodReflection $prototype, ClassReflection $pr $method->getName(), $i + 1, $prototypeParameter->getName(), - $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), ))->identifier('parameter.variadic'); @@ -220,7 +221,7 @@ public function compare(ExtendedMethodReflection $prototype, ClassReflection $pr $method->getName(), $i + 1, $prototypeParameter->getName(), - $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), ))->identifier('parameter.notOptional'); @@ -246,7 +247,7 @@ public function compare(ExtendedMethodReflection $prototype, ClassReflection $pr $i + 1, $prototypeParameter->getName(), $prototypeParameterType->describe(VerbosityLevel::typeOnly()), - $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), ))->identifier('method.childParameterType'); @@ -274,7 +275,7 @@ public function compare(ExtendedMethodReflection $prototype, ClassReflection $pr $i + 1, $prototypeParameter->getName(), $prototypeParameterType->describe(VerbosityLevel::typeOnly()), - $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), ))->identifier('method.childParameterType'); @@ -294,7 +295,7 @@ public function compare(ExtendedMethodReflection $prototype, ClassReflection $pr $i + 1, $prototypeParameter->getName(), $prototypeParameterType->describe(VerbosityLevel::typeOnly()), - $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), ))->identifier('method.childParameterType'); @@ -394,6 +395,9 @@ private function isTypeCompatible(Type $methodParameterType, Type $prototypePara if ($prototypeParameterType instanceof ArrayType) { return true; } + if ($prototypeParameterType instanceof ConstantArrayType) { + return true; + } if ($prototypeParameterType->isObject()->yes() && $prototypeParameterType->getObjectClassNames() === [Traversable::class]) { return true; } 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 f7a1a8120b..2e585ff43e 100644 --- a/src/Rules/Methods/MethodSignatureRule.php +++ b/src/Rules/Methods/MethodSignatureRule.php @@ -7,10 +7,8 @@ use PHPStan\Node\InClassMethodNode; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; -use PHPStan\Reflection\Php\NativeBuiltinMethodReflection; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\Php\PhpClassReflectionExtension; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; @@ -19,6 +17,7 @@ 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; @@ -37,14 +36,13 @@ /** * @implements Rule */ -class MethodSignatureRule implements Rule +final class MethodSignatureRule implements Rule { public function __construct( private PhpClassReflectionExtension $phpClassReflectionExtension, private bool $reportMaybes, private bool $reportStatic, - private bool $abstractTraitMethod, ) { } @@ -67,8 +65,6 @@ 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, $parentMethodDeclaringClass]) { @@ -76,8 +72,8 @@ public function processNode(Node $node, Scope $scope): array if (count($parentVariants) !== 1) { continue; } - $parentParameters = ParametersAcceptorSelector::selectSingle($parentVariants); - [$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)) { $builder = RuleErrorBuilder::message(sprintf( 'Return type (%s) of method %s::%s() should be %s with return type (%s) of method %s::%s()', @@ -116,7 +112,7 @@ public function processNode(Node $node, Scope $scope): array $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; @@ -124,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, @@ -169,30 +165,28 @@ private function collectParentMethods(string $methodName, ClassReflection $class $parentMethods[] = [$method, $method->getDeclaringClass()]; } - if ($this->abstractTraitMethod) { - foreach ($class->getTraits(true) as $trait) { - $nativeTraitReflection = $trait->getNativeReflection(); - if (!$nativeTraitReflection->hasMethod($methodName)) { - continue; - } - - $methodReflection = $nativeTraitReflection->getMethod($methodName); - $isAbstract = $methodReflection->isAbstract(); - if (!$isAbstract) { - continue; - } + foreach ($class->getTraits(true) as $trait) { + $nativeTraitReflection = $trait->getNativeReflection(); + if (!$nativeTraitReflection->hasMethod($methodName)) { + continue; + } - $declaringTrait = $trait->getNativeMethod($methodName)->getDeclaringClass(); - $parentMethods[] = [ - $this->phpClassReflectionExtension->createUserlandMethodReflection( - $trait, - $class, - new NativeBuiltinMethodReflection($methodReflection), - $declaringTrait->getName(), - ), - $declaringTrait, - ]; + $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; @@ -203,8 +197,8 @@ private function collectParentMethods(string $methodName, ClassReflection $class */ private function checkReturnTypeCompatibility( ClassReflection $declaringClass, - ParametersAcceptorWithPhpDocs $currentVariant, - ParametersAcceptorWithPhpDocs $parentVariant, + ExtendedParametersAcceptor $currentVariant, + ExtendedParametersAcceptor $parentVariant, ): array { $returnType = TypehintHelper::decideType( @@ -226,15 +220,15 @@ private function checkReturnTypeCompatibility( return [TrinaryLogic::createYes(), $returnType, $parentReturnType]; } - return [$parentReturnType->isSuperTypeOf($returnType), TypehintHelper::decideType( + return [$parentReturnType->isSuperTypeOf($returnType)->result, TypehintHelper::decideType( $currentVariant->getNativeReturnType(), $currentVariant->getPhpDocReturnType(), ), $originalParentReturnType]; } /** - * @param ParameterReflectionWithPhpDocs[] $parameters - * @param ParameterReflectionWithPhpDocs[] $parentParameters + * @param ExtendedParameterReflection[] $parameters + * @param ExtendedParameterReflection[] $parentParameters * @return array */ private function checkParameterTypeCompatibility( @@ -260,7 +254,7 @@ private function checkParameterTypeCompatibility( ); $parentParameterType = $this->transformStaticType($declaringClass, $originalParameterType); - $parameterResults[] = [$parameterType->isSuperTypeOf($parentParameterType), TypehintHelper::decideType( + $parameterResults[] = [$parameterType->isSuperTypeOf($parentParameterType)->result, TypehintHelper::decideType( $parameter->getNativeType(), $parameter->getPhpDocType(), ), $originalParameterType]; @@ -272,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 index 2bb1fb915d..485959b362 100644 --- a/src/Rules/Methods/MethodVisibilityInInterfaceRule.php +++ b/src/Rules/Methods/MethodVisibilityInInterfaceRule.php @@ -4,13 +4,15 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassMethodNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; /** @implements Rule */ -class MethodVisibilityInInterfaceRule implements Rule +#[RegisteredRule(level: 0)] +final class MethodVisibilityInInterfaceRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Methods/MissingMagicSerializationMethodsRule.php b/src/Rules/Methods/MissingMagicSerializationMethodsRule.php index a169d86794..604afa8666 100644 --- a/src/Rules/Methods/MissingMagicSerializationMethodsRule.php +++ b/src/Rules/Methods/MissingMagicSerializationMethodsRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; @@ -16,7 +17,8 @@ /** * @implements Rule */ -class MissingMagicSerializationMethodsRule implements Rule +#[RegisteredRule(level: 0)] +final class MissingMagicSerializationMethodsRule implements Rule { public function __construct(private PhpVersion $phpversion) diff --git a/src/Rules/Methods/MissingMethodImplementationRule.php b/src/Rules/Methods/MissingMethodImplementationRule.php index e7b6d2c3bb..3f4d7dd136 100644 --- a/src/Rules/Methods/MissingMethodImplementationRule.php +++ b/src/Rules/Methods/MissingMethodImplementationRule.php @@ -5,6 +5,7 @@ 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; @@ -13,7 +14,8 @@ /** * @implements Rule */ -class MissingMethodImplementationRule implements Rule +#[RegisteredRule(level: 0)] +final class MissingMethodImplementationRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Methods/MissingMethodParameterTypehintRule.php b/src/Rules/Methods/MissingMethodParameterTypehintRule.php index 5ef4db93d1..9961a60254 100644 --- a/src/Rules/Methods/MissingMethodParameterTypehintRule.php +++ b/src/Rules/Methods/MissingMethodParameterTypehintRule.php @@ -4,9 +4,9 @@ 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\IdentifierRuleError; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; @@ -14,18 +14,17 @@ use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** * @implements Rule */ +#[RegisteredRule(level: 6)] final class MissingMethodParameterTypehintRule implements Rule { public function __construct( private MissingTypehintCheck $missingTypehintCheck, - private bool $paramOut, ) { } @@ -40,7 +39,7 @@ public function processNode(Node $node, Scope $scope): array $methodReflection = $node->getMethodReflection(); $messages = []; - foreach (ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getParameters() as $parameterReflection) { + foreach ($methodReflection->getParameters() as $parameterReflection) { foreach ($this->checkMethodParameter($methodReflection, sprintf('parameter $%s', $parameterReflection->getName()), $parameterReflection->getType()) as $parameterMessage) { $messages[] = $parameterMessage; } @@ -51,9 +50,6 @@ public function processNode(Node $node, Scope $scope): array } } - if (!$this->paramOut) { - continue; - } if ($parameterReflection->getOutType() === null) { continue; } @@ -104,7 +100,7 @@ private function checkMethodParameter(MethodReflection $methodReflection, string $methodReflection->getName(), $parameterMessage, $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Methods/MissingMethodReturnTypehintRule.php b/src/Rules/Methods/MissingMethodReturnTypehintRule.php index e0e6685ae9..370f59c6c4 100644 --- a/src/Rules/Methods/MissingMethodReturnTypehintRule.php +++ b/src/Rules/Methods/MissingMethodReturnTypehintRule.php @@ -4,19 +4,19 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassMethodNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** * @implements Rule */ +#[RegisteredRule(level: 6)] final class MissingMethodReturnTypehintRule implements Rule { @@ -39,7 +39,7 @@ public function processNode(Node $node, Scope $scope): array return []; } } - $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + $returnType = $methodReflection->getReturnType(); if ($returnType instanceof MixedType && !$returnType->isExplicitMixed()) { return [ @@ -71,7 +71,7 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); 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 008f4e7d08..5e97ca0bf9 100644 --- a/src/Rules/Methods/NullsafeMethodCallRule.php +++ b/src/Rules/Methods/NullsafeMethodCallRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; @@ -12,7 +13,8 @@ /** * @implements Rule */ -class NullsafeMethodCallRule implements Rule +#[RegisteredRule(level: 4)] +final class NullsafeMethodCallRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Methods/OverridingMethodRule.php b/src/Rules/Methods/OverridingMethodRule.php index 17c4f09990..f1d3044b75 100644 --- a/src/Rules/Methods/OverridingMethodRule.php +++ b/src/Rules/Methods/OverridingMethodRule.php @@ -3,18 +3,14 @@ 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\ClassReflection; -use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; +use PHPStan\Reflection\ExtendedFunctionVariant; use PHPStan\Reflection\MethodPrototypeReflection; -use PHPStan\Reflection\Native\NativeMethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\NativeBuiltinMethodReflection; -use PHPStan\Reflection\Php\PhpClassReflectionExtension; -use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -29,17 +25,19 @@ /** * @implements Rule */ -class OverridingMethodRule implements Rule +#[RegisteredRule(level: 0)] +final class OverridingMethodRule implements Rule { public function __construct( private PhpVersion $phpVersion, private MethodSignatureRule $methodSignatureRule, + #[AutowiredParameter] private bool $checkPhpDocMethodSignatures, private MethodParameterComparisonHelper $methodParameterComparisonHelper, - private PhpClassReflectionExtension $phpClassReflectionExtension, - private bool $genericPrototypeMessage, - private bool $finalByPhpDoc, + private MethodVisibilityComparisonHelper $methodVisibilityComparisonHelper, + private MethodPrototypeFinder $methodPrototypeFinder, + #[AutowiredParameter] private bool $checkMissingOverrideMethodAttribute, ) { @@ -53,7 +51,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $method = $node->getMethodReflection(); - $prototypeData = $this->findPrototype($node->getClassReflection(), $method->getName()); + $prototypeData = $this->methodPrototypeFinder->findPrototype($node->getClassReflection(), $method->getName()); if ($prototypeData === null) { if (strtolower($method->getName()) === '__construct') { $parent = $method->getDeclaringClass()->getParentClass(); @@ -65,7 +63,7 @@ public function processNode(Node $node, Scope $scope): array 'Method %s::%s() overrides final method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $parent->getDisplayName($this->genericPrototypeMessage), + $parent->getDisplayName(true), $parentConstructor->getName(), )) ->nonIgnorable() @@ -73,13 +71,13 @@ public function processNode(Node $node, Scope $scope): array ->build(), ], $node, $scope); } - if ($parentConstructor->isFinal()->yes() && $this->finalByPhpDoc) { + 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($this->genericPrototypeMessage), + $parent->getDisplayName(true), $parentConstructor->getName(), ))->identifier('method.parentMethodFinalByPhpDoc') ->build(), @@ -97,6 +95,10 @@ public function processNode(Node $node, Scope $scope): array )) ->nonIgnorable() ->identifier('method.override') + ->fixNode($node->getOriginalNode(), function (Node\Stmt\ClassMethod $method) { + $method->attrGroups = $this->filterOverrideAttribute($method->attrGroups); + return $method; + }) ->build(), ]; } @@ -110,33 +112,43 @@ public function processNode(Node $node, Scope $scope): array 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($this->genericPrototypeMessage), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), - ))->identifier('method.missingOverride')->build(); + )) + ->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(), - $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), )) ->nonIgnorable() ->identifier('method.parentMethodFinal') ->build(); - } elseif ($prototype->isFinal()->yes() && $this->finalByPhpDoc) { + } elseif ($prototype->isFinal()->yes()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Method %s::%s() overrides @final method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), ))->identifier('method.parentMethodFinalByPhpDoc') ->build(); @@ -148,7 +160,7 @@ public function processNode(Node $node, Scope $scope): array 'Non-static method %s::%s() overrides static method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), )) ->nonIgnorable() @@ -160,7 +172,7 @@ public function processNode(Node $node, Scope $scope): array 'Static method %s::%s() overrides non-static method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), )) ->nonIgnorable() @@ -169,32 +181,7 @@ public function processNode(Node $node, Scope $scope): array } if ($checkVisibility) { - if ($prototype->isPublic()) { - if (!$method->isPublic()) { - $messages[] = RuleErrorBuilder::message(sprintf( - '%s method %s::%s() overriding public method %s::%s() should also be public.', - $method->isPrivate() ? 'Private' : 'Protected', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), - $prototype->getName(), - )) - ->nonIgnorable() - ->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($this->genericPrototypeMessage), - $prototype->getName(), - )) - ->nonIgnorable() - ->identifier('method.visibility') - ->build(); - } + $messages = array_merge($messages, $this->methodVisibilityComparisonHelper->compare($prototype, $prototypeDeclaringClass, $method)); } $prototypeVariants = $prototype->getVariants(); @@ -204,8 +191,7 @@ public function processNode(Node $node, Scope $scope): array $prototypeVariant = $prototypeVariants[0]; - $methodVariant = ParametersAcceptorSelector::selectSingle($method->getVariants()); - $methodReturnType = $methodVariant->getNativeReturnType(); + $methodReturnType = $method->getNativeReturnType(); $realPrototype = $method->getPrototype(); @@ -216,14 +202,14 @@ public function processNode(Node $node, Scope $scope): array && !$this->hasReturnTypeWillChangeAttribute($node->getOriginalNode()) && count($prototypeDeclaringClass->getNativeReflection()->getMethod($prototype->getName())->getAttributes('ReturnTypeWillChange')) === 0 ) { - if (!$this->methodParameterComparisonHelper->isReturnTypeCompatible($realPrototype->getTentativeReturnType(), $methodVariant->getNativeReturnType(), true)) { + if (!$this->methodParameterComparisonHelper->isReturnTypeCompatible($realPrototype->getTentativeReturnType(), $method->getNativeReturnType(), true)) { $messages[] = RuleErrorBuilder::message(sprintf( 'Return type %s of method %s::%s() is not covariant with tentative return type %s of method %s::%s().', $methodReturnType->describe(VerbosityLevel::typeOnly()), $method->getDeclaringClass()->getDisplayName(), $method->getName(), $realPrototype->getTentativeReturnType()->describe(VerbosityLevel::typeOnly()), - $realPrototype->getDeclaringClass()->getDisplayName($this->genericPrototypeMessage), + $realPrototype->getDeclaringClass()->getDisplayName(true), $realPrototype->getName(), )) ->tip('Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.') @@ -233,19 +219,24 @@ public function processNode(Node $node, Scope $scope): array } } - $messages = array_merge($messages, $this->methodParameterComparisonHelper->compare($prototype, $prototypeDeclaringClass, $method)); + $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 || $prototype->isInternal()->no(); + $reportReturnType = !$realPrototype instanceof MethodPrototypeReflection + || $realPrototype->getTentativeReturnType() === null + || (is_bool($prototype->isBuiltin()) ? !$prototype->isBuiltin() : $prototype->isBuiltin()->no()); } else { if ($realPrototype instanceof MethodPrototypeReflection && $realPrototype->isInternal()) { - if ($prototype->isInternal()->yes() && $prototypeDeclaringClass->getName() !== $realPrototype->getDeclaringClass()->getName()) { + if ( + (is_bool($prototype->isBuiltin()) ? $prototype->isBuiltin() : $prototype->isBuiltin()->yes()) + && $prototypeDeclaringClass->getName() !== $realPrototype->getDeclaringClass()->getName() + ) { $realPrototypeVariant = $realPrototype->getVariants()[0]; if ( $prototypeReturnType instanceof MixedType @@ -256,7 +247,10 @@ public function processNode(Node $node, Scope $scope): array } } - if ($reportReturnType && $prototype->isInternal()->yes()) { + if ( + $reportReturnType + && (is_bool($prototype->isBuiltin()) ? $prototype->isBuiltin() : $prototype->isBuiltin()->yes()) + ) { $reportReturnType = !$this->hasReturnTypeWillChangeAttribute($node->getOriginalNode()); } } @@ -273,7 +267,7 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $prototypeReturnType->describe(VerbosityLevel::typeOnly()), - $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), )) ->nonIgnorable() @@ -286,7 +280,7 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $prototypeReturnType->describe(VerbosityLevel::typeOnly()), - $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), )) ->nonIgnorable() @@ -298,6 +292,30 @@ public function processNode(Node $node, Scope $scope): array return $this->addErrors($messages, $node, $scope); } + /** + * @param Node\AttributeGroup[] $attrGroups + * @return Node\AttributeGroup[] + */ + private function filterOverrideAttribute(array $attrGroups): array + { + foreach ($attrGroups as $i => $attrGroup) { + foreach ($attrGroup->attrs as $j => $attr) { + if ($attr->name->toLowerString() !== 'override') { + continue; + } + + unset($attrGroup->attrs[$j]); + if (count($attrGroup->attrs) !== 0) { + continue; + } + + unset($attrGroups[$i]); + } + } + + return $attrGroups; + } + /** * @param list $errors * @return list @@ -345,77 +363,4 @@ private function hasOverrideAttribute(Node\Stmt\ClassMethod $method): bool return false; } - /** - * @return array{ExtendedMethodReflection, ClassReflection, bool}|null - */ - private function findPrototype(ClassReflection $classReflection, string $methodName): ?array - { - foreach ($classReflection->getImmediateInterfaces() as $immediateInterface) { - if ($immediateInterface->hasNativeMethod($methodName)) { - $method = $immediateInterface->getNativeMethod($methodName); - return [$method, $method->getDeclaringClass(), true]; - } - } - - if ($this->phpVersion->supportsAbstractTraitMethods()) { - foreach ($classReflection->getTraits(true) as $trait) { - $nativeTraitReflection = $trait->getNativeReflection(); - if (!$nativeTraitReflection->hasMethod($methodName)) { - continue; - } - - $methodReflection = $nativeTraitReflection->getMethod($methodName); - $isAbstract = $methodReflection->isAbstract(); - if ($isAbstract) { - $declaringTrait = $trait->getNativeMethod($methodName)->getDeclaringClass(); - return [ - $this->phpClassReflectionExtension->createUserlandMethodReflection( - $trait, - $classReflection, - new NativeBuiltinMethodReflection($methodReflection), - $declaringTrait->getName(), - ), - $declaringTrait, - false, - ]; - } - } - } - - $parentClass = $classReflection->getParentClass(); - if ($parentClass === null) { - return null; - } - - if (!$parentClass->hasNativeMethod($methodName)) { - return null; - } - - $method = $parentClass->getNativeMethod($methodName); - if ($method->isPrivate()) { - return null; - } - - $declaringClass = $method->getDeclaringClass(); - if ($declaringClass->hasConstructor()) { - if ($method->getName() === $declaringClass->getConstructor()->getName()) { - $prototype = $method->getPrototype(); - if ($prototype instanceof PhpMethodReflection || $prototype instanceof MethodPrototypeReflection || $prototype instanceof NativeMethodReflection) { - $abstract = $prototype->isAbstract(); - if (is_bool($abstract)) { - if (!$abstract) { - return null; - } - } elseif (!$abstract->yes()) { - return null; - } - } - } elseif (strtolower($methodName) === '__construct') { - return null; - } - } - - return [$method, $method->getDeclaringClass(), true]; - } - } diff --git a/src/Rules/Methods/ReturnTypeRule.php b/src/Rules/Methods/ReturnTypeRule.php index d1140961a5..9c9882fedf 100644 --- a/src/Rules/Methods/ReturnTypeRule.php +++ b/src/Rules/Methods/ReturnTypeRule.php @@ -5,7 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\FunctionReturnTypeCheck; use PHPStan\Rules\IdentifierRuleError; @@ -22,11 +22,13 @@ use function count; use function sprintf; use function strtolower; +use function ucfirst; /** * @implements Rule */ -class ReturnTypeRule implements Rule +#[RegisteredRule(level: 3)] +final class ReturnTypeRule implements Rule { public function __construct(private FunctionReturnTypeCheck $returnTypeCheck) @@ -53,31 +55,38 @@ public function processNode(Node $node, Scope $scope): array return []; } - $returnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType(); + if ($method->isPropertyHook()) { + $methodDescription = sprintf( + '%s hook for property %s::$%s', + ucfirst($method->getPropertyHookName()), + $method->getDeclaringClass()->getDisplayName(), + $method->getHookedPropertyName(), + ); + } else { + $methodDescription = sprintf('Method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()); + } + + $returnType = $method->getReturnType(); $errors = $this->returnTypeCheck->checkReturnType( $scope, $returnType, $node->expr, $node, sprintf( - 'Method %s::%s() should return %%s but empty return statement found.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), + '%s should return %%s but empty return statement found.', + $methodDescription, ), sprintf( - 'Method %s::%s() with return type void returns %%s but should not return anything.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), + '%s with return type void returns %%s but should not return anything.', + $methodDescription, ), sprintf( - 'Method %s::%s() should return %%s but returns %%s.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), + '%s should return %%s but returns %%s.', + $methodDescription, ), sprintf( - 'Method %s::%s() should never return but return statement found.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), + '%s should never return but return statement found.', + $methodDescription, ), $method->isGenerator(), ); @@ -87,7 +96,7 @@ public function processNode(Node $node, Scope $scope): array && $errors[0]->getIdentifier() === 'return.type' && !$errors[0] instanceof TipRuleError && $errors[0] instanceof LineRuleError - && $method->getDeclaringClass()->isSubclassOf(Rule::class) + && $method->getDeclaringClass()->is(Rule::class) && strtolower($method->getName()) === 'processnode' && $node->expr !== null ) { diff --git a/src/Rules/Methods/StaticMethodCallCheck.php b/src/Rules/Methods/StaticMethodCallCheck.php index d50f8dfd32..b0a6f057e5 100644 --- a/src/Rules/Methods/StaticMethodCallCheck.php +++ b/src/Rules/Methods/StaticMethodCallCheck.php @@ -7,6 +7,8 @@ use PhpParser\Node\Name; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MethodReflection; @@ -15,6 +17,7 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -32,14 +35,19 @@ use function sprintf; use function strtolower; -class StaticMethodCallCheck +#[AutowiredService] +final class StaticMethodCallCheck { public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, private ClassNameCheck $classCheck, + #[AutowiredParameter] private bool $checkFunctionNameCase, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, + #[AutowiredParameter] private bool $reportMagicMethods, ) { @@ -119,19 +127,36 @@ public function check( return [[], null]; } + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'Call to static method %s() on an unknown class %s.', + $methodName, + $className, + )) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + return [ [ - RuleErrorBuilder::message(sprintf( - 'Call to static method %s() on an unknown class %s.', - $methodName, - $className, - ))->identifier('class.notFound')->discoveringSymbolsTip()->build(), + $errorBuilder->build(), ], null, ]; } - $errors = $this->classCheck->checkClassNames([new ClassNameNodePair($className, $class)]); + $locationData = []; + $locationClassReflection = $this->reflectionProvider->getClass($className); + if ($locationClassReflection->hasMethod($methodName)) { + $locationData['method'] = $locationClassReflection->getMethod($methodName, $scope); + } + + $errors = $this->classCheck->checkClassNames( + $scope, + [new ClassNameNodePair($className, $class)], + ClassNameUsageLocation::from(ClassNameUsageLocation::STATIC_METHOD_CALL, $locationData), + ); $classType = $scope->resolveTypeByName($class); } @@ -221,7 +246,7 @@ public function check( $classType->getObjectClassNames(), static fn (string $objectClassName) => TrinaryLogic::createFromBoolean( $scope->isInClass() - && ($scope->getClassReflection()->getName() === $objectClassName || $scope->getClassReflection()->isSubclassOf($objectClassName)), + && $scope->getClassReflection()->is($objectClassName), ), ); if ( diff --git a/src/Rules/Methods/StaticMethodCallableRule.php b/src/Rules/Methods/StaticMethodCallableRule.php index a9bae8d9d5..e1b33d1321 100644 --- a/src/Rules/Methods/StaticMethodCallableRule.php +++ b/src/Rules/Methods/StaticMethodCallableRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Php\PhpVersion; @@ -14,7 +15,8 @@ /** * @implements Rule */ -class StaticMethodCallableRule implements Rule +#[RegisteredRule(level: 0)] +final class StaticMethodCallableRule implements Rule { public function __construct(private StaticMethodCallCheck $methodCallCheck, private PhpVersion $phpVersion) diff --git a/src/Rules/Missing/MissingReturnRule.php b/src/Rules/Missing/MissingReturnRule.php index 71102e4f23..5bc9d3d40e 100644 --- a/src/Rules/Missing/MissingReturnRule.php +++ b/src/Rules/Missing/MissingReturnRule.php @@ -5,9 +5,10 @@ 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; @@ -20,15 +21,19 @@ use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; use function sprintf; +use function ucfirst; /** * @implements Rule */ -class MissingReturnRule implements Rule +#[RegisteredRule(level: 0)] +final class MissingReturnRule implements Rule { public function __construct( + #[AutowiredParameter] private bool $checkExplicitMixedMissingReturn, + #[AutowiredParameter] private bool $checkPhpDocMissingReturn, ) { @@ -55,9 +60,13 @@ public function processNode(Node $node, Scope $scope): array return []; } } elseif ($scopeFunction !== null) { - $returnType = ParametersAcceptorSelector::selectSingle($scopeFunction->getVariants())->getReturnType(); - if ($scopeFunction instanceof MethodReflection) { - $description = sprintf('Method %s::%s()', $scopeFunction->getDeclaringClass()->getDisplayName(), $scopeFunction->getName()); + $returnType = $scopeFunction->getReturnType(); + if ($scopeFunction instanceof PhpMethodFromParserNodeReflection) { + if (!$scopeFunction->isPropertyHook()) { + $description = sprintf('Method %s::%s()', $scopeFunction->getDeclaringClass()->getDisplayName(), $scopeFunction->getName()); + } else { + $description = sprintf('%s hook for property %s::$%s', ucfirst($scopeFunction->getPropertyHookName()), $scopeFunction->getDeclaringClass()->getDisplayName(), $scopeFunction->getHookedPropertyName()); + } } else { $description = sprintf('Function %s()', $scopeFunction->getName()); } diff --git a/src/Rules/MissingTypehintCheck.php b/src/Rules/MissingTypehintCheck.php index ae407a2a30..e92904cd09 100644 --- a/src/Rules/MissingTypehintCheck.php +++ b/src/Rules/MissingTypehintCheck.php @@ -6,6 +6,8 @@ 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; @@ -13,6 +15,7 @@ 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; @@ -21,13 +24,17 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use Traversable; +use function array_filter; use function array_keys; use function array_merge; +use function count; +use function implode; use function in_array; use function sprintf; use function strtolower; -class MissingTypehintCheck +#[AutowiredService] +final class MissingTypehintCheck { public const MISSING_ITERABLE_VALUE_TYPE_TIP = 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type'; @@ -43,10 +50,9 @@ class MissingTypehintCheck * @param string[] $skipCheckGenericClasses */ public function __construct( - private bool $disableCheckMissingIterableValueType, - private bool $checkMissingIterableValueType, - private bool $checkGenericClassInNonGenericObjectType, + #[AutowiredParameter] private bool $checkMissingCallableSignature, + #[AutowiredParameter(ref: '%featureToggles.skipCheckGenericClasses%')] private array $skipCheckGenericClasses, ) { @@ -57,12 +63,6 @@ public function __construct( */ public function getIterableTypesWithMissingValueTypehint(Type $type): array { - if (!$this->checkMissingIterableValueType) { - if (!$this->disableCheckMissingIterableValueType) { - return []; - } - } - $iterablesWithMissingValueTypehint = []; TypeTraverser::map($type, function (Type $type, callable $traverse) use (&$iterablesWithMissingValueTypehint): Type { if ($type instanceof TemplateType) { @@ -85,7 +85,11 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array if ($iterableValue instanceof MixedType && !$iterableValue->isExplicitMixed()) { $iterablesWithMissingValueTypehint[] = $type; } - if ($type instanceof IntersectionType && !$type->isList()->yes()) { + if ($type instanceof IntersectionType) { + if ($type->isList()->yes()) { + return $traverse($iterableValue); + } + return $type; } } @@ -96,17 +100,13 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array } /** - * @return array + * @return array */ public function getNonGenericObjectTypesWithGenericClass(Type $type): array { - if (!$this->checkGenericClassInNonGenericObjectType) { - return []; - } - $objectTypes = []; TypeTraverser::map($type, function (Type $type, callable $traverse) use (&$objectTypes): Type { - if ($type instanceof GenericObjectType) { + if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) { $traverse($type); return $type; } @@ -136,9 +136,22 @@ public function getNonGenericObjectTypesWithGenericClass(Type $type): array if (!$resolvedType instanceof ObjectType) { throw new ShouldNotHappenException(); } + + $templateTypes = $classReflection->getTemplateTypeMap()->getTypes(); + $templateTypesCount = count($templateTypes); + $requiredTemplateTypesCount = count(array_filter($templateTypes, static fn (Type $type) => $type instanceof TemplateType && $type->getDefault() === null)); + if ($requiredTemplateTypesCount === 0) { + return $type; + } + + $templateTypesList = implode(', ', array_keys($templateTypes)); + if ($requiredTemplateTypesCount !== $templateTypesCount) { + $templateTypesList .= sprintf(' (%d-%d required)', $requiredTemplateTypesCount, $templateTypesCount); + } + $objectTypes[] = [ sprintf('%s %s', strtolower($classReflection->getClassTypeDescription()), $classReflection->getDisplayName(false)), - array_keys($classReflection->getTemplateTypeMap()->getTypes()), + $templateTypesList, ]; return $type; } diff --git a/src/Rules/Names/UsedNamesRule.php b/src/Rules/Names/UsedNamesRule.php index a1afbb742b..61dd8f285c 100644 --- a/src/Rules/Names/UsedNamesRule.php +++ b/src/Rules/Names/UsedNamesRule.php @@ -10,8 +10,8 @@ use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Trait_; use PhpParser\Node\Stmt\Use_; -use PhpParser\Node\Stmt\UseUse; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\FileNode; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; @@ -23,6 +23,7 @@ /** * @implements Rule */ +#[RegisteredRule(level: 0)] final class UsedNamesRule implements Rule { @@ -100,7 +101,7 @@ private function findErrorsForNode(Node $node, string $namespace, array &$usedNa $namespace !== '' ? $namespace . '\\' . $node->name->toString() : $node->name->toString(), )) ->identifier(sprintf('%s.nameInUse', $type)) - ->line($node->getLine()) + ->line($node->getStartLine()) ->nonIgnorable() ->build(), ]; @@ -113,7 +114,7 @@ private function findErrorsForNode(Node $node, string $namespace, array &$usedNa } /** - * @param UseUse[] $uses + * @param Node\UseItem[] $uses * @param array $usedNames * @return list */ @@ -132,7 +133,7 @@ private function findErrorsInUses(array $uses, string $useGroupPrefix, string $l $use->getAlias()->toString(), )) ->identifier('use.nameInUse') - ->line($use->getLine()) + ->line($use->getStartLine()) ->nonIgnorable() ->build(); continue; @@ -142,7 +143,7 @@ private function findErrorsInUses(array $uses, string $useGroupPrefix, string $l return $errors; } - private function shouldBeIgnored(Use_|GroupUse|UseUse $use): bool + 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 b961e235fc..6885c42817 100644 --- a/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php +++ b/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php @@ -5,6 +5,8 @@ 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\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; @@ -19,13 +21,17 @@ /** * @implements Rule */ -class ExistingNamesInGroupUseRule implements Rule +#[RegisteredRule(level: 0)] +final class ExistingNamesInGroupUseRule implements Rule { public function __construct( private ReflectionProvider $reflectionProvider, private ClassNameCheck $classCheck, + #[AutowiredParameter] private bool $checkFunctionNameCase, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { } @@ -54,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array ) { $error = $this->checkFunction($name); } elseif ($use->type === Use_::TYPE_NORMAL) { - $error = $this->checkClass($name); + $error = $this->checkClass($scope, $name); } else { throw new ShouldNotHappenException(); } @@ -72,11 +78,15 @@ public function processNode(Node $node, Scope $scope): array 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() + $errorBuilder = RuleErrorBuilder::message(sprintf('Used constant %s not found.', (string) $name)) ->line($name->getStartLine()) - ->identifier('constant.notFound') - ->build(); + ->identifier('constant.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + return $errorBuilder->build(); } return null; @@ -85,11 +95,15 @@ private function checkConstant(Node\Name $name): ?IdentifierRuleError 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() + $errorBuilder = RuleErrorBuilder::message(sprintf('Used function %s not found.', (string) $name)) ->line($name->getStartLine()) - ->identifier('function.notFound') - ->build(); + ->identifier('function.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + return $errorBuilder->build(); } if ($this->checkFunctionNameCase) { @@ -114,11 +128,11 @@ private function checkFunction(Node\Name $name): ?IdentifierRuleError return null; } - private function checkClass(Node\Name $name): ?IdentifierRuleError + private function checkClass(Scope $scope, Node\Name $name): ?IdentifierRuleError { - $errors = $this->classCheck->checkClassNames([ + $errors = $this->classCheck->checkClassNames($scope, [ new ClassNameNodePair((string) $name, $name), - ]); + ], null); if (count($errors) === 0) { return null; } elseif (count($errors) === 1) { diff --git a/src/Rules/Namespaces/ExistingNamesInUseRule.php b/src/Rules/Namespaces/ExistingNamesInUseRule.php index 92415f012c..73490e22c5 100644 --- a/src/Rules/Namespaces/ExistingNamesInUseRule.php +++ b/src/Rules/Namespaces/ExistingNamesInUseRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; @@ -18,13 +20,17 @@ /** * @implements Rule */ -class ExistingNamesInUseRule implements Rule +#[RegisteredRule(level: 0)] +final class ExistingNamesInUseRule implements Rule { public function __construct( private ReflectionProvider $reflectionProvider, private ClassNameCheck $classCheck, + #[AutowiredParameter] private bool $checkFunctionNameCase, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { } @@ -54,11 +60,11 @@ public function processNode(Node $node, Scope $scope): array return $this->checkFunctions($node->uses); } - return $this->checkClasses($node->uses); + return $this->checkClasses($scope, $node->uses); } /** - * @param Node\Stmt\UseUse[] $uses + * @param Node\UseItem[] $uses * @return list */ private function checkConstants(array $uses): array @@ -69,18 +75,22 @@ private function checkConstants(array $uses): array continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Used constant %s not found.', (string) $use->name)) + $errorBuilder = RuleErrorBuilder::message(sprintf('Used constant %s not found.', (string) $use->name)) ->line($use->name->getStartLine()) - ->identifier('constant.notFound') - ->discoveringSymbolsTip() - ->build(); + ->identifier('constant.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); } return $errors; } /** - * @param Node\Stmt\UseUse[] $uses + * @param Node\UseItem[] $uses * @return list */ private function checkFunctions(array $uses): array @@ -88,11 +98,15 @@ 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)) + $errorBuilder = RuleErrorBuilder::message(sprintf('Used function %s not found.', (string) $use->name)) ->line($use->name->getStartLine()) - ->identifier('function.notFound') - ->discoveringSymbolsTip() - ->build(); + ->identifier('function.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); } elseif ($this->checkFunctionNameCase) { $functionReflection = $this->reflectionProvider->getFunction($use->name, null); $realName = $functionReflection->getName(); @@ -117,13 +131,15 @@ private function checkFunctions(array $uses): array } /** - * @param Node\Stmt\UseUse[] $uses + * @param Node\UseItem[] $uses * @return list */ - private function checkClasses(array $uses): array + private function checkClasses(Scope $scope, array $uses): array { return $this->classCheck->checkClassNames( - array_map(static fn (Node\Stmt\UseUse $use): ClassNameNodePair => new ClassNameNodePair((string) $use->name, $use->name), $uses), + $scope, + array_map(static fn (Node\UseItem $use): ClassNameNodePair => new ClassNameNodePair((string) $use->name, $use->name), $uses), + null, ); } 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 4f6e564849..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,7 +16,8 @@ /** * @implements Rule */ -class InvalidAssignVarRule implements Rule +#[RegisteredRule(level: 0)] +final class InvalidAssignVarRule implements Rule { public function __construct(private NullsafeCheck $nullsafeCheck) @@ -85,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 2c42f7be1e..374aa87bfc 100644 --- a/src/Rules/Operators/InvalidBinaryOperationRule.php +++ b/src/Rules/Operators/InvalidBinaryOperationRule.php @@ -5,11 +5,13 @@ 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; @@ -20,13 +22,13 @@ /** * @implements Rule */ -class InvalidBinaryOperationRule implements Rule +#[RegisteredRule(level: 2)] +final class InvalidBinaryOperationRule implements Rule { public function __construct( private ExprPrinter $exprPrinter, private RuleLevelHelper $ruleLevelHelper, - private bool $bleedingEdge, ) { } @@ -45,10 +47,6 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (!$scope->getType($node) instanceof ErrorType && !$this->bleedingEdge) { - return []; - } - $leftName = '__PHPSTAN__LEFT__'; $rightName = '__PHPSTAN__RIGHT__'; $leftVariable = new Node\Expr\Variable($leftName); @@ -104,8 +102,8 @@ public function processNode(Node $node, Scope $scope): array } $scope = $scope - ->assignVariable($leftName, $leftType, $leftType) - ->assignVariable($rightName, $rightType, $rightType); + ->assignVariable($leftName, $leftType, $leftType, TrinaryLogic::createYes()) + ->assignVariable($rightName, $rightType, $rightType, TrinaryLogic::createYes()); if (!$scope->getType($newNode) instanceof ErrorType) { return []; diff --git a/src/Rules/Operators/InvalidComparisonOperationRule.php b/src/Rules/Operators/InvalidComparisonOperationRule.php index 43b8760503..e3a0af0e6f 100644 --- a/src/Rules/Operators/InvalidComparisonOperationRule.php +++ b/src/Rules/Operators/InvalidComparisonOperationRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -23,7 +24,8 @@ /** * @implements Rule */ -class InvalidComparisonOperationRule implements Rule +#[RegisteredRule(level: 2)] +final class InvalidComparisonOperationRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) diff --git a/src/Rules/Operators/InvalidIncDecOperationRule.php b/src/Rules/Operators/InvalidIncDecOperationRule.php index 9cd551cbf3..c52a790ef6 100644 --- a/src/Rules/Operators/InvalidIncDecOperationRule.php +++ b/src/Rules/Operators/InvalidIncDecOperationRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -24,13 +25,12 @@ /** * @implements Rule */ -class InvalidIncDecOperationRule implements Rule +#[RegisteredRule(level: 0)] +final class InvalidIncDecOperationRule implements Rule { public function __construct( private RuleLevelHelper $ruleLevelHelper, - private bool $bleedingEdge, - private bool $checkThisOnly, ) { } @@ -87,30 +87,16 @@ public function processNode(Node $node, Scope $scope): array ]; } - if (!$this->bleedingEdge) { - if ($this->checkThisOnly) { - 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(); - $varType = $scope->getType($node->var); - if (!$varType->toString() instanceof ErrorType) { - return []; - } - if (!$varType->toNumber() instanceof ErrorType) { - return []; - } - } else { - $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(); - - if ($varType instanceof ErrorType || $allowedTypes->isSuperTypeOf($varType)->yes()) { - return []; - } + if ($varType instanceof ErrorType || $allowedTypes->isSuperTypeOf($varType)->yes()) { + return []; } return [ diff --git a/src/Rules/Operators/InvalidUnaryOperationRule.php b/src/Rules/Operators/InvalidUnaryOperationRule.php index 099c1d6507..1b688dc06d 100644 --- a/src/Rules/Operators/InvalidUnaryOperationRule.php +++ b/src/Rules/Operators/InvalidUnaryOperationRule.php @@ -5,10 +5,12 @@ 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; @@ -17,12 +19,12 @@ /** * @implements Rule */ -class InvalidUnaryOperationRule implements Rule +#[RegisteredRule(level: 2)] +final class InvalidUnaryOperationRule implements Rule { public function __construct( private RuleLevelHelper $ruleLevelHelper, - private bool $bleedingEdge, ) { } @@ -42,38 +44,34 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($this->bleedingEdge) { - $varName = '__PHPSTAN__LEFT__'; - $variable = new Node\Expr\Variable($varName); - $newNode = clone $node; - $newNode->setAttribute('phpstan_cache_printer', null); - $newNode->expr = $variable; + $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; - } + 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 []; - } + $exprType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->expr, + '', + $callback, + )->getType(); + if ($exprType instanceof ErrorType) { + return []; + } - if (!$scope instanceof MutatingScope) { - throw new ShouldNotHappenException(); - } + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } - $scope = $scope->assignVariable($varName, $exprType, $exprType); - if (!$scope->getType($newNode) instanceof ErrorType) { - return []; - } - } elseif (!$scope->getType($node) instanceof ErrorType) { + $scope = $scope->assignVariable($varName, $exprType, $exprType, TrinaryLogic::createYes()); + if (!$scope->getType($newNode) instanceof ErrorType) { return []; } diff --git a/src/Rules/ParameterCastableToStringCheck.php b/src/Rules/ParameterCastableToStringCheck.php index e34f6fba22..244cd1ba4f 100644 --- a/src/Rules/ParameterCastableToStringCheck.php +++ b/src/Rules/ParameterCastableToStringCheck.php @@ -5,13 +5,15 @@ use PhpParser\Node\Arg; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ParameterReflection; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function sprintf; -class ParameterCastableToStringCheck +#[AutowiredService] +final class ParameterCastableToStringCheck { public function __construct(private RuleLevelHelper $ruleLevelHelper) @@ -36,11 +38,13 @@ public function checkParameter( $scope, $parameter->value, '', - static fn (Type $type): bool => !$castFn($type->getIterableValueType()) instanceof ErrorType, + static fn (Type $type): bool => $type->isArray()->yes() && !$castFn($type->getIterableValueType()) instanceof ErrorType, ); - if ($typeResult->getType() instanceof ErrorType - || !$castFn($typeResult->getType()->getIterableValueType()) instanceof ErrorType) { + if ( + ! $typeResult->getType()->isArray()->yes() + || !$castFn($typeResult->getType()->getIterableValueType()) instanceof ErrorType + ) { return null; } diff --git a/src/Rules/PhpDoc/AssertRuleHelper.php b/src/Rules/PhpDoc/AssertRuleHelper.php index e7d3f8e73b..c999630d24 100644 --- a/src/Rules/PhpDoc/AssertRuleHelper.php +++ b/src/Rules/PhpDoc/AssertRuleHelper.php @@ -2,41 +2,71 @@ namespace PHPStan\Rules\PhpDoc; +use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Stmt\Function_; +use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\TypeExpr; +use PHPStan\PhpDoc\Tag\AssertTag; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\ClassNameCheck; +use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; +use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ErrorType; use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; use function array_key_exists; +use function array_merge; use function sprintf; use function substr; -class AssertRuleHelper +#[AutowiredService] +final class AssertRuleHelper { - public function __construct(private InitializerExprTypeResolver $initializerExprTypeResolver) + public function __construct( + private InitializerExprTypeResolver $initializerExprTypeResolver, + private ReflectionProvider $reflectionProvider, + private UnresolvableTypeHelper $unresolvableTypeHelper, + private ClassNameCheck $classCheck, + private MissingTypehintCheck $missingTypehintCheck, + private GenericObjectTypeCheck $genericObjectTypeCheck, + #[AutowiredParameter] + private bool $checkClassCaseSensitivity, + #[AutowiredParameter] + private bool $checkMissingTypehints, + ) { } /** * @return list */ - public function check(ExtendedMethodReflection|FunctionReflection $reflection, ParametersAcceptor $acceptor): array + 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) { + if ($reflection instanceof ExtendedMethodReflection && !$reflection->isStatic()) { $class = $reflection->getDeclaringClass(); - $parametersByName['this'] = new ObjectType($class->getName(), null, $class); + $parametersByName['this'] = new ObjectType($class->getName(), classReflection: $class); } $context = InitializerExprContext::createEmpty(); @@ -57,38 +87,133 @@ public function check(ExtendedMethodReflection|FunctionReflection $reflection, P $assertedExpr = $assert->getParameter()->getExpr(new TypeExpr($parametersByName[$parameterName])); $assertedExprType = $this->initializerExprTypeResolver->getType($assertedExpr, $context); + $assertedExprString = $assert->getParameter()->describe(); if ($assertedExprType instanceof ErrorType) { + $errors[] = RuleErrorBuilder::message(sprintf('Assert references unknown %s.', $assertedExprString)) + ->identifier('assert.unknownExpr') + ->build(); continue; } $assertedType = $assert->getType(); + $tagName = [ + AssertTag::NULL => '@phpstan-assert', + AssertTag::IF_TRUE => '@phpstan-assert-if-true', + AssertTag::IF_FALSE => '@phpstan-assert-if-false', + ][$assert->getIf()]; + + if ($this->unresolvableTypeHelper->containsUnresolvableType($assertedType)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for %s contains unresolvable type.', + $tagName, + $assertedExprString, + ))->identifier('assert.unresolvableType')->build(); + continue; + } + $isSuperType = $assertedType->isSuperTypeOf($assertedExprType); - if ($isSuperType->maybe()) { + if (!$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; } - $assertedExprString = $assert->getParameter()->describe(); + 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(); + } - if ($assert->isNegated() ? $isSuperType->yes() : $isSuperType->no()) { + foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($assertedType) as [$innerName, $genericTypeNames]) { $errors[] = RuleErrorBuilder::message(sprintf( - 'Asserted %stype %s for %s with type %s can never happen.', - $assert->isNegated() ? 'negated ' : '', - $assertedType->describe(VerbosityLevel::precise()), + 'PHPDoc tag %s for %s contains generic %s but does not specify its types: %s', + $tagName, $assertedExprString, - $assertedExprType->describe(VerbosityLevel::precise()), - ))->identifier('assert.impossibleType')->build(); - } elseif ($assert->isNegated() ? $isSuperType->no() : $isSuperType->yes()) { + $innerName, + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); + } + + foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($assertedType) as $callableType) { $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()), + 'PHPDoc tag %s for %s has no signature specified for %s.', + $tagName, $assertedExprString, - $assertedExprType->describe(VerbosityLevel::precise()), - ))->identifier('assert.alreadyNarrowedType')->build(); + $callableType->describe(VerbosityLevel::typeOnly()), + ))->identifier('missingType.callable')->build(); } } - return $errors; } diff --git a/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php b/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php index 7ad14e65b6..af7cfda155 100644 --- a/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php +++ b/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php @@ -2,7 +2,8 @@ namespace PHPStan\Rules\PhpDoc; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ConditionalType; @@ -17,13 +18,14 @@ use function sprintf; use function substr; -class ConditionalReturnTypeRuleHelper +#[AutowiredService] +final class ConditionalReturnTypeRuleHelper { /** * @return list */ - public function check(ParametersAcceptorWithPhpDocs $acceptor): array + public function check(ExtendedParametersAcceptor $acceptor): array { $conditionalTypes = []; $parametersByName = []; diff --git a/src/Rules/PhpDoc/FunctionAssertRule.php b/src/Rules/PhpDoc/FunctionAssertRule.php index 0995b7574a..46eba83a07 100644 --- a/src/Rules/PhpDoc/FunctionAssertRule.php +++ b/src/Rules/PhpDoc/FunctionAssertRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InFunctionNode; use PHPStan\Rules\Rule; use function count; @@ -11,7 +12,8 @@ /** * @implements Rule */ -class FunctionAssertRule implements Rule +#[RegisteredRule(level: 2)] +final class FunctionAssertRule implements Rule { public function __construct(private AssertRuleHelper $helper) @@ -31,7 +33,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - return $this->helper->check($function, $variants[0]); + return $this->helper->check($scope, $node->getOriginalNode(), $function, $variants[0]); } } diff --git a/src/Rules/PhpDoc/FunctionConditionalReturnTypeRule.php b/src/Rules/PhpDoc/FunctionConditionalReturnTypeRule.php index 5610c04925..b5a1a713bf 100644 --- a/src/Rules/PhpDoc/FunctionConditionalReturnTypeRule.php +++ b/src/Rules/PhpDoc/FunctionConditionalReturnTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InFunctionNode; use PHPStan\Rules\Rule; use function count; @@ -11,7 +12,8 @@ /** * @implements Rule */ -class FunctionConditionalReturnTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class FunctionConditionalReturnTypeRule implements Rule { public function __construct(private ConditionalReturnTypeRuleHelper $helper) diff --git a/src/Rules/PhpDoc/GenericCallableRuleHelper.php b/src/Rules/PhpDoc/GenericCallableRuleHelper.php index 1fdb7a17c0..d38279a659 100644 --- a/src/Rules/PhpDoc/GenericCallableRuleHelper.php +++ b/src/Rules/PhpDoc/GenericCallableRuleHelper.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\Generics\TemplateTypeCheck; @@ -18,7 +19,8 @@ use function array_keys; use function sprintf; -class GenericCallableRuleHelper +#[AutowiredService] +final class GenericCallableRuleHelper { public function __construct( @@ -60,6 +62,9 @@ public function check( 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(); diff --git a/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php index 3b89aea5c3..5d946e4488 100644 --- a/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\Generics\GenericObjectTypeCheck; @@ -20,7 +21,8 @@ /** * @implements Rule */ -class IncompatibleClassConstantPhpDocTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class IncompatibleClassConstantPhpDocTypeRule implements Rule { public function __construct( 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 e37db8ae89..e5a82505fc 100644 --- a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php @@ -5,33 +5,24 @@ use PhpParser\Node; use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; -use PHPStan\Internal\SprintfHelper; -use PHPStan\PhpDoc\Tag\ParamOutTag; -use PHPStan\PhpDoc\Tag\ParamTag; -use PHPStan\Rules\Generics\GenericObjectTypeCheck; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Type; -use PHPStan\Type\VerbosityLevel; -use function array_merge; use function is_string; -use function sprintf; use function trim; /** * @implements Rule */ -class IncompatiblePhpDocTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class IncompatiblePhpDocTypeRule implements Rule { public function __construct( private FileTypeMapper $fileTypeMapper, - private GenericObjectTypeCheck $genericObjectTypeCheck, - private UnresolvableTypeHelper $unresolvableTypeHelper, - private GenericCallableRuleHelper $genericCallableRuleHelper, + private IncompatiblePhpDocTypeCheck $check, ) { } @@ -63,187 +54,20 @@ public function processNode(Node $node, Scope $scope): array $functionName, $docComment->getText(), ); - $nativeParameterTypes = $this->getNativeParameterTypes($node, $scope); - $byRefParameters = $this->getByRefParameters($node); - $errors = []; - - foreach ([$resolvedPhpDoc->getParamTags(), $resolvedPhpDoc->getParamOutTags()] as $parameters) { - foreach ($parameters as $parameterName => $phpDocParamTag) { - $phpDocParamType = $phpDocParamTag->getType(); - $tagName = $phpDocParamTag instanceof ParamTag ? '@param' : '@param-out'; - - 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(); - } - $isParamSuperType = $nativeParamType->isSuperTypeOf($phpDocParamType); - - $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 ($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 ($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 { - $nativeReturnType = $this->getNativeReturnType($node, $scope); - $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; + return $this->check->check( + $scope, + $node, + $resolvedPhpDoc, + $functionName, + $this->getNativeParameterTypes($node, $scope), + $this->getByRefParameters($node), + $this->getNativeReturnType($node, $scope), + ); } /** - * @return Type[] + * @return array */ private function getNativeParameterTypes(Node\FunctionLike $node, Scope $scope): array { 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 4713a0cd08..6ebc2e639c 100644 --- a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php @@ -4,13 +4,13 @@ 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\ParserNodeTypeToPHPStanType; use PHPStan\Type\VerbosityLevel; use function array_merge; use function sprintf; @@ -18,7 +18,8 @@ /** * @implements Rule */ -class IncompatiblePropertyPhpDocTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class IncompatiblePropertyPhpDocTypeRule implements Rule { public function __construct( @@ -62,33 +63,35 @@ public function processNode(Node $node, Scope $scope): array ))->identifier('property.unresolvableType')->build(); } - $nativeType = ParserNodeTypeToPHPStanType::resolve($node->getNativeType(), $classReflection); - $isSuperType = $nativeType->isSuperTypeOf($phpDocType); - if ($isSuperType->no()) { - $messages[] = RuleErrorBuilder::message(sprintf( - '%s for property %s::$%s with type %s is incompatible with native type %s.', - $description, - $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()))); + $nativeType = $node->getNativeType(); + if ($nativeType !== null) { + $isSuperType = $nativeType->isSuperTypeOf($phpDocType); + if ($isSuperType->no()) { + $messages[] = RuleErrorBuilder::message(sprintf( + '%s for property %s::$%s with type %s is incompatible with native type %s.', + $description, + $classReflection->getDisplayName(), + $propertyName, + $phpDocType->describe(VerbosityLevel::typeOnly()), + $nativeType->describe(VerbosityLevel::typeOnly()), + ))->identifier('property.phpDocType')->build(); + + } elseif ($isSuperType->maybe()) { + $errorBuilder = RuleErrorBuilder::message(sprintf( + '%s for property %s::$%s with type %s is not subtype of native type %s.', + $description, + $classReflection->getDisplayName(), + $propertyName, + $phpDocType->describe(VerbosityLevel::typeOnly()), + $nativeType->describe(VerbosityLevel::typeOnly()), + ))->identifier('property.phpDocType'); + + if ($phpDocType instanceof TemplateType) { + $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocType->getName(), $nativeType->describe(VerbosityLevel::typeOnly()))); + } + + $messages[] = $errorBuilder->build(); } - - $messages[] = $errorBuilder->build(); } $className = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); diff --git a/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php index 22be14246c..9efce61b95 100644 --- a/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php +++ b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php @@ -4,19 +4,31 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassMethodNode; +use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; +use function array_merge; use function sprintf; /** * @implements Rule */ -class IncompatibleSelfOutTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class IncompatibleSelfOutTypeRule implements Rule { + public function __construct( + private UnresolvableTypeHelper $unresolvableTypeHelper, + private GenericObjectTypeCheck $genericObjectTypeCheck, + ) + { + } + public function getNodeType(): string { return InClassMethodNode::class; @@ -32,21 +44,62 @@ public function processNode(Node $node, Scope $scope): array } $classReflection = $method->getDeclaringClass(); - $classType = new ObjectType($classReflection->getName(), null, $classReflection); + $classType = new ObjectType($classReflection->getName(), classReflection: $classReflection); - if ($classType->isSuperTypeOf($selfOutType)->yes()) { - return []; - } - - return [ - RuleErrorBuilder::message(sprintf( + $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->getName(), + $classReflection->getDisplayName(), $method->getName(), $classType->describe(VerbosityLevel::precise()), - ))->identifier('selfOut.type')->build(), - ]; + ))->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 5d4f65420c..d909dfab72 100644 --- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -3,7 +3,9 @@ namespace PHPStan\Rules\PhpDoc; use PhpParser\Node; +use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\VirtualNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\PhpDocParser; @@ -15,9 +17,10 @@ use function str_starts_with; /** - * @implements Rule + * @implements Rule */ -class InvalidPHPStanDocTagRule implements Rule +#[RegisteredRule(level: 2)] +final class InvalidPHPStanDocTagRule implements Rule { private const POSSIBLE_PHPSTAN_TAGS = [ @@ -55,6 +58,7 @@ class InvalidPHPStanDocTagRule implements Rule '@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', @@ -63,39 +67,26 @@ class InvalidPHPStanDocTagRule implements Rule public function __construct( private Lexer $phpDocLexer, private PhpDocParser $phpDocParser, - private bool $checkAllInvalidPhpDocs, ) { } public function getNodeType(): string { - return Node::class; + return NodeAbstract::class; } public function processNode(Node $node, Scope $scope): array { - if (!$this->checkAllInvalidPhpDocs) { - if ( - !$node instanceof Node\Stmt\ClassLike - && !$node instanceof Node\FunctionLike - && !$node instanceof Node\Stmt\Foreach_ - && !$node instanceof Node\Stmt\Property - && !$node instanceof Node\Expr\Assign - && !$node instanceof Node\Expr\AssignRef - && !$node instanceof Node\Stmt\ClassConst - ) { - return []; - } - } else { - // mirrored with InvalidPhpDocTagValueRule - if ($node instanceof VirtualNode) { - return []; - } - if ($node instanceof Node\Stmt\Expression) { - return []; - } - if ($node instanceof Node\Expr && !$node instanceof Node\Expr\Assign && !$node instanceof Node\Expr\AssignRef) { + // 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 []; } } diff --git a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php index 6734c54e4c..4b18ca789a 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php @@ -2,9 +2,10 @@ namespace PHPStan\Rules\PhpDoc; -use Nette\Utils\Strings; 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; @@ -18,48 +19,35 @@ use function str_starts_with; /** - * @implements Rule + * @implements Rule */ -class InvalidPhpDocTagValueRule implements Rule +#[RegisteredRule(level: 2)] +final class InvalidPhpDocTagValueRule implements Rule { public function __construct( private Lexer $phpDocLexer, private PhpDocParser $phpDocParser, - private bool $checkAllInvalidPhpDocs, - private bool $invalidPhpDocTagLine, ) { } public function getNodeType(): string { - return Node::class; + return NodeAbstract::class; } public function processNode(Node $node, Scope $scope): array { - if (!$this->checkAllInvalidPhpDocs) { - if ( - !$node instanceof Node\Stmt\ClassLike - && !$node instanceof Node\FunctionLike - && !$node instanceof Node\Stmt\Foreach_ - && !$node instanceof Node\Stmt\Property - && !$node instanceof Node\Expr\Assign - && !$node instanceof Node\Expr\AssignRef - && !$node instanceof Node\Stmt\ClassConst - ) { - return []; - } - } else { - // mirrored with InvalidPHPStanDocTagRule - if ($node instanceof VirtualNode) { - return []; - } - if ($node instanceof Node\Stmt\Expression) { - return []; - } - if ($node instanceof Node\Expr && !$node instanceof Node\Expr\Assign && !$node instanceof Node\Expr\AssignRef) { + // 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 []; } } @@ -88,7 +76,7 @@ public function processNode(Node $node, Scope $scope): array 'PHPDoc tag %s %s has invalid value: %s', $phpDocTag->name, $phpDocTag->value->alias, - $this->trimExceptionMessage($phpDocTag->value->type->getException()->getMessage()), + $phpDocTag->value->type->getException()->getMessage(), )) ->line(PhpDocLineHelper::detectLine($node, $phpDocTag)) ->identifier('phpDoc.parseError')->build(); @@ -102,7 +90,7 @@ public function processNode(Node $node, Scope $scope): array 'PHPDoc tag %s has invalid value (%s): %s', $phpDocTag->name, $phpDocTag->value->value, - $this->trimExceptionMessage($phpDocTag->value->exception->getMessage()), + $phpDocTag->value->exception->getMessage(), )) ->line(PhpDocLineHelper::detectLine($node, $phpDocTag)) ->identifier('phpDoc.parseError')->build(); @@ -111,13 +99,4 @@ public function processNode(Node $node, Scope $scope): array return $errors; } - private function trimExceptionMessage(string $message): string - { - if ($this->invalidPhpDocTagLine) { - return $message; - } - - return Strings::replace($message, '~( on line \d+)$~', ''); - } - } diff --git a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php index 029f558bb8..eadf89f84b 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php @@ -4,10 +4,13 @@ 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\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; @@ -16,14 +19,14 @@ use PHPStan\Type\VerbosityLevel; use function array_map; use function array_merge; -use function implode; use function is_string; use function sprintf; /** * @implements Rule */ -class InvalidPhpDocVarTagTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class InvalidPhpDocVarTagTypeRule implements Rule { public function __construct( @@ -33,8 +36,12 @@ public function __construct( private GenericObjectTypeCheck $genericObjectTypeCheck, private MissingTypehintCheck $missingTypehintCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, + #[AutowiredParameter] private bool $checkClassCaseSensitivity, + #[AutowiredParameter] private bool $checkMissingVarTagTypehint, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { } @@ -48,7 +55,6 @@ public function processNode(Node $node, Scope $scope): array { if ( $node instanceof Node\Stmt\Property - || $node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Stmt\ClassConst || $node instanceof Node\Stmt\Const_ ) { @@ -98,6 +104,17 @@ public function processNode(Node $node, Scope $scope): array ->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); @@ -111,17 +128,6 @@ public function processNode(Node $node, Scope $scope): array 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), - )) - ->identifier('missingType.generics') - ->build(); - } - $referencedClasses = $varTagType->getReferencedClasses(); foreach ($referencedClasses as $referencedClass) { if ($this->reflectionProvider->hasClass($referencedClass)) { @@ -138,19 +144,25 @@ public function processNode(Node $node, Scope $scope): array continue; } - $errors[] = RuleErrorBuilder::message(sprintf( + $errorBuilder = RuleErrorBuilder::message(sprintf( sprintf('%s contains unknown class %%s.', $identifier), $referencedClass, )) - ->identifier('class.notFound') - ->discoveringSymbolsTip() - ->build(); + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); } $errors = array_merge( $errors, $this->classCheck->checkClassNames( + $scope, array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $node), $referencedClasses), + ClassNameUsageLocation::from(ClassNameUsageLocation::PHPDOC_TAG_VAR), $this->checkClassCaseSensitivity, ), ); diff --git a/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php b/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php index 1efb15ffaa..9af949a91f 100644 --- a/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php +++ b/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php @@ -3,7 +3,10 @@ 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; @@ -16,9 +19,10 @@ use function sprintf; /** - * @implements Rule + * @implements Rule */ -class InvalidThrowsPhpDocValueRule implements Rule +#[RegisteredRule(level: 2)] +final class InvalidThrowsPhpDocValueRule implements Rule { public function __construct(private FileTypeMapper $fileTypeMapper) @@ -27,13 +31,17 @@ public function __construct(private FileTypeMapper $fileTypeMapper) public function getNodeType(): string { - return Node\Stmt::class; + return NodeAbstract::class; } public function processNode(Node $node, Scope $scope): array { - if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) { - return []; // is handled by virtual nodes + if ($node instanceof Node\Stmt) { + if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) { + return []; // is handled by virtual nodes + } + } elseif (!$node instanceof InPropertyHookNode) { + return []; } $docComment = $node->getDocComment(); diff --git a/src/Rules/PhpDoc/MethodAssertRule.php b/src/Rules/PhpDoc/MethodAssertRule.php index 5c24f9bca8..fbfcf14eaf 100644 --- a/src/Rules/PhpDoc/MethodAssertRule.php +++ b/src/Rules/PhpDoc/MethodAssertRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassMethodNode; use PHPStan\Rules\Rule; use function count; @@ -11,7 +12,8 @@ /** * @implements Rule */ -class MethodAssertRule implements Rule +#[RegisteredRule(level: 2)] +final class MethodAssertRule implements Rule { public function __construct(private AssertRuleHelper $helper) @@ -31,7 +33,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - return $this->helper->check($method, $variants[0]); + return $this->helper->check($scope, $node->getOriginalNode(), $method, $variants[0]); } } diff --git a/src/Rules/PhpDoc/MethodConditionalReturnTypeRule.php b/src/Rules/PhpDoc/MethodConditionalReturnTypeRule.php index 6925576665..7ef6b6e2f3 100644 --- a/src/Rules/PhpDoc/MethodConditionalReturnTypeRule.php +++ b/src/Rules/PhpDoc/MethodConditionalReturnTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassMethodNode; use PHPStan\Rules\Rule; use function count; @@ -11,7 +12,8 @@ /** * @implements Rule */ -class MethodConditionalReturnTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class MethodConditionalReturnTypeRule implements Rule { public function __construct(private ConditionalReturnTypeRuleHelper $helper) diff --git a/src/Rules/PhpDoc/PhpDocLineHelper.php b/src/Rules/PhpDoc/PhpDocLineHelper.php index d1d4b52e8b..a7894f762f 100644 --- a/src/Rules/PhpDoc/PhpDocLineHelper.php +++ b/src/Rules/PhpDoc/PhpDocLineHelper.php @@ -5,7 +5,7 @@ use PhpParser\Node as PhpParserNode; use PHPStan\PhpDocParser\Ast\Node as PhpDocNode; -class PhpDocLineHelper +final class PhpDocLineHelper { /** @@ -19,7 +19,7 @@ public static function detectLine(PhpParserNode $node, PhpDocNode $phpDocNode): $phpDoc = $node->getDocComment(); if ($phpDocTagLine === null || $phpDoc === null) { - return $node->getLine(); + return $node->getStartLine(); } return $phpDoc->getStartLine() + $phpDocTagLine - 1; diff --git a/src/Rules/PhpDoc/RequireExtendsCheck.php b/src/Rules/PhpDoc/RequireExtendsCheck.php index 14f6d43647..6323fb6a83 100644 --- a/src/Rules/PhpDoc/RequireExtendsCheck.php +++ b/src/Rules/PhpDoc/RequireExtendsCheck.php @@ -3,24 +3,34 @@ namespace PHPStan\Rules\PhpDoc; use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\PhpDoc\Tag\RequireExtendsTag; use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; +use function array_column; +use function array_map; use function array_merge; use function count; +use function sort; use function sprintf; use function strtolower; +#[AutowiredService] final class RequireExtendsCheck { public function __construct( private ClassNameCheck $classCheck, + #[AutowiredParameter] private bool $checkClassCaseSensitivity, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { } @@ -29,7 +39,7 @@ public function __construct( * @param array $extendsTags * @return list */ - public function checkExtendsTags(Node $node, array $extendsTags): array + public function checkExtendsTags(Scope $scope, Node $node, array $extendsTags): array { $errors = []; @@ -41,39 +51,52 @@ public function checkExtendsTags(Node $node, array $extendsTags): array foreach ($extendsTags as $extendsTag) { $type = $extendsTag->getType(); - if (!$type instanceof ObjectType) { + $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; } - $class = $type->getClassName(); - $referencedClassReflection = $type->getClassReflection(); + 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 ($referencedClassReflection === null) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends contains unknown class %s.', $class)) - ->discoveringSymbolsTip() - ->identifier('class.notFound') - ->build(); - continue; - } + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } - if (!$referencedClassReflection->isClass()) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends cannot contain non-class type %s.', $class)) - ->identifier(sprintf('requireExtends.%s', strtolower($referencedClassReflection->getClassTypeDescription()))) - ->build(); - } elseif ($referencedClassReflection->isFinal()) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends cannot contain final class %s.', $class)) - ->identifier('requireExtends.finalClass') - ->build(); - } else { - $errors = array_merge( - $errors, - $this->classCheck->checkClassNames([ - new ClassNameNodePair($class, $node), - ], $this->checkClassCaseSensitivity), - ); + $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), + ); + } } } diff --git a/src/Rules/PhpDoc/RequireExtendsDefinitionClassRule.php b/src/Rules/PhpDoc/RequireExtendsDefinitionClassRule.php index 27d8b3d6a3..5c56c41d0a 100644 --- a/src/Rules/PhpDoc/RequireExtendsDefinitionClassRule.php +++ b/src/Rules/PhpDoc/RequireExtendsDefinitionClassRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -13,7 +14,8 @@ /** * @implements Rule */ -class RequireExtendsDefinitionClassRule implements Rule +#[RegisteredRule(level: 2)] +final class RequireExtendsDefinitionClassRule implements Rule { public function __construct( @@ -44,7 +46,7 @@ public function processNode(Node $node, Scope $scope): array ]; } - return $this->requireExtendsCheck->checkExtendsTags($node, $extendsTags); + return $this->requireExtendsCheck->checkExtendsTags($scope, $node, $extendsTags); } } diff --git a/src/Rules/PhpDoc/RequireExtendsDefinitionTraitRule.php b/src/Rules/PhpDoc/RequireExtendsDefinitionTraitRule.php index 174e7c9385..6f69533d53 100644 --- a/src/Rules/PhpDoc/RequireExtendsDefinitionTraitRule.php +++ b/src/Rules/PhpDoc/RequireExtendsDefinitionTraitRule.php @@ -4,13 +4,15 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; /** * @implements Rule */ -class RequireExtendsDefinitionTraitRule implements Rule +#[RegisteredRule(level: 2)] +final class RequireExtendsDefinitionTraitRule implements Rule { public function __construct( @@ -37,7 +39,7 @@ public function processNode(Node $node, Scope $scope): array $traitReflection = $this->reflectionProvider->getClass($node->namespacedName->toString()); $extendsTags = $traitReflection->getRequireExtendsTags(); - return $this->requireExtendsCheck->checkExtendsTags($node, $extendsTags); + return $this->requireExtendsCheck->checkExtendsTags($scope, $node, $extendsTags); } } diff --git a/src/Rules/PhpDoc/RequireImplementsDefinitionClassRule.php b/src/Rules/PhpDoc/RequireImplementsDefinitionClassRule.php index 03e9422e2a..f012a0dfff 100644 --- a/src/Rules/PhpDoc/RequireImplementsDefinitionClassRule.php +++ b/src/Rules/PhpDoc/RequireImplementsDefinitionClassRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -13,7 +14,8 @@ /** * @implements Rule */ -class RequireImplementsDefinitionClassRule implements Rule +#[RegisteredRule(level: 2)] +final class RequireImplementsDefinitionClassRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php b/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php index 2c18b2e3ef..f27d022dd2 100644 --- a/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php +++ b/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php @@ -4,27 +4,36 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; +use function array_column; +use function array_map; use function array_merge; +use function count; use function sprintf; use function strtolower; /** * @implements Rule */ -class RequireImplementsDefinitionTraitRule implements Rule +#[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, ) { } @@ -49,34 +58,42 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($implementsTags as $implementsTag) { $type = $implementsTag->getType(); - if (!$type instanceof ObjectType) { + $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; } - $class = $type->getClassName(); - $referencedClassReflection = $type->getClassReflection(); - if ($referencedClassReflection === null) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-implements contains unknown class %s.', $class)) - ->discoveringSymbolsTip() - ->identifier('class.notFound') - ->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 (!$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([ - new ClassNameNodePair($class, $node), - ], $this->checkClassCaseSensitivity), - ); + 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), + ); + } } } 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 index 3968f05792..6632d6320b 100644 --- a/src/Rules/PhpDoc/VarTagChangedExpressionTypeRule.php +++ b/src/Rules/PhpDoc/VarTagChangedExpressionTypeRule.php @@ -4,13 +4,15 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\VarTagChangedExpressionTypeNode; use PHPStan\Rules\Rule; /** * @implements Rule */ -class VarTagChangedExpressionTypeRule implements Rule +#[RegisteredRule(level: 2)] +final class VarTagChangedExpressionTypeRule implements Rule { public function __construct(private VarTagTypeRuleHelper $varTagTypeRuleHelper) diff --git a/src/Rules/PhpDoc/VarTagTypeRuleHelper.php b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php index d01b17396b..c8c9c212b8 100644 --- a/src/Rules/PhpDoc/VarTagTypeRuleHelper.php +++ b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php @@ -4,12 +4,19 @@ use PhpParser\Node; use PhpParser\Node\Expr; +use PHPStan\Analyser\NameScope; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\GetOffsetValueTypeExpr; +use PHPStan\PhpDoc\NameScopeAlreadyBeingCreatedException; use PHPStan\PhpDoc\Tag\VarTag; +use PHPStan\PhpDoc\TypeNodeResolver; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ArrayType; +use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; @@ -21,10 +28,19 @@ use function is_string; use function sprintf; -class VarTagTypeRuleHelper +#[AutowiredService] +final class VarTagTypeRuleHelper { - public function __construct(private bool $checkTypeAgainstPhpDocType, private bool $strictWideningCheck) + public function __construct( + private TypeNodeResolver $typeNodeResolver, + private FileTypeMapper $fileTypeMapper, + private ReflectionProvider $reflectionProvider, + #[AutowiredParameter(ref: '%reportWrongPhpDocTypeInVarTag%')] + private bool $checkTypeAgainstPhpDocType, + #[AutowiredParameter(ref: '%reportAnyTypeWideningInVarTag%')] + private bool $strictWideningCheck, + ) { } @@ -53,7 +69,7 @@ public function checkVarType(Scope $scope, Node\Expr $var, Node\Expr $expr, arra continue; } if ($arrayItem->key === null) { - $dimExpr = new Node\Scalar\LNumber($i); + $dimExpr = new Node\Scalar\Int_($i); } else { $dimExpr = $arrayItem->key; } @@ -76,7 +92,7 @@ public function checkExprType(Scope $scope, Node\Expr $expr, Type $varTagType): $errors = []; $exprNativeType = $scope->getNativeType($expr); $containsPhpStanType = $this->containsPhpStanType($varTagType); - if ($this->shouldVarTagTypeBeReported($expr, $exprNativeType, $varTagType)) { + 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.', @@ -86,7 +102,7 @@ public function checkExprType(Scope $scope, Node\Expr $expr, Type $varTagType): } else { $exprType = $scope->getType($expr); if ( - $this->shouldVarTagTypeBeReported($expr, $exprType, $varTagType) + $this->shouldVarTagTypeBeReported($scope, $expr, $exprType, $varTagType) && ($this->checkTypeAgainstPhpDocType || $containsPhpStanType) ) { $verbosity = VerbosityLevel::getRecommendedLevelByType($exprType, $varTagType); @@ -116,8 +132,13 @@ public function checkExprType(Scope $scope, Node\Expr $expr, Type $varTagType): 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->isSubclassOf(Type::class)) { + if (!$classReflection->isSubclassOfClass($typeClass)) { continue; } @@ -127,22 +148,22 @@ private function containsPhpStanType(Type $type): bool return false; } - private function shouldVarTagTypeBeReported(Node\Expr $expr, Type $type, Type $varTagType): bool + 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 $type->isSuperTypeOf($varTagType)->no(); + return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType); } if ($expr instanceof Expr\ConstFetch) { - return $type->isSuperTypeOf($varTagType)->no(); + return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType); } if ($expr instanceof Node\Scalar) { - return $type->isSuperTypeOf($varTagType)->no(); + return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType); } if ($expr instanceof Expr\New_) { @@ -151,24 +172,24 @@ private function shouldVarTagTypeBeReported(Node\Expr $expr, Type $type, Type $v } } - return $this->checkType($type, $varTagType); + return $this->checkType($scope, $type, $varTagType); } - private function checkType(Type $type, Type $varTagType, int $depth = 0): bool + private function checkType(Scope $scope, Type $type, Type $varTagType, int $depth = 0): bool { if ($this->strictWideningCheck) { - return !$type->isSuperTypeOf($varTagType)->yes(); + return !$this->isSuperTypeOfVarType($scope, $type, $varTagType); } if ($type->isConstantArray()->yes()) { if ($type->isIterableAtLeastOnce()->no()) { $type = new ArrayType(new MixedType(), new MixedType()); - return $type->isSuperTypeOf($varTagType)->no(); + return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType); } } if ($type->isIterable()->yes() && $varTagType->isIterable()->yes()) { - if ($type->isSuperTypeOf($varTagType)->no()) { + if (!$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType)) { return true; } @@ -176,17 +197,62 @@ private function checkType(Type $type, Type $varTagType, int $depth = 0): bool $innerVarTagType = $varTagType->getIterableValueType(); if ($type->equals($innerType) || $varTagType->equals($innerVarTagType)) { - return !$innerType->isSuperTypeOf($innerVarTagType)->yes(); + return !$this->isSuperTypeOfVarType($scope, $innerType, $innerVarTagType); } - return $this->checkType($innerType, $innerVarTagType, $depth + 1); + 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; } - if ($type->isConstantValue()->yes() && $depth === 0) { - return $type->isSuperTypeOf($varTagType)->no(); + try { + $type = $this->typeNodeResolver->resolve($type->toPhpDocNode(), $this->createNameScope($scope)); + } catch (NameScopeAlreadyBeingCreatedException) { + return true; } - return !$type->isSuperTypeOf($varTagType)->yes(); + 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 031edf1743..a517f4e38e 100644 --- a/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php +++ b/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php @@ -6,6 +6,7 @@ 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; @@ -31,13 +32,13 @@ /** * @implements Rule */ -class WrongVariableNameInVarTagRule implements Rule +#[RegisteredRule(level: 2)] +final class WrongVariableNameInVarTagRule implements Rule { public function __construct( private FileTypeMapper $fileTypeMapper, private VarTagTypeRuleHelper $varTagTypeRuleHelper, - private bool $checkTypeAgainstNativeType, ) { } @@ -51,7 +52,6 @@ public function processNode(Node $node, Scope $scope): array { if ( $node instanceof Node\Stmt\Property - || $node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Stmt\ClassConst || $node instanceof Node\Stmt\Const_ || ($node instanceof VirtualNode && !$node instanceof InFunctionNode && !$node instanceof InClassMethodNode && !$node instanceof InClassNode) @@ -90,10 +90,13 @@ public function processNode(Node $node, Scope $scope): array } 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); } @@ -136,11 +139,6 @@ private function processAssign(Scope $scope, Node\Expr $var, Node\Expr $expr, ar $errors = []; $hasMultipleMessage = false; $assignedVariables = $this->getAssignedVariables($var); - if ($this->checkTypeAgainstNativeType) { - foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $var, $expr, $varTags, $assignedVariables) as $error) { - $errors[] = $error; - } - } foreach (array_keys($varTags) as $key) { if (is_int($key)) { if (count($varTags) !== 1) { @@ -179,6 +177,12 @@ private function processAssign(Scope $scope, Node\Expr $var, Node\Expr $expr, ar } } + if (count($errors) === 0) { + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $var, $expr, $varTags, $assignedVariables) as $error) { + $errors[] = $error; + } + } + return $errors; } @@ -195,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) { @@ -249,19 +253,17 @@ private function processForeach(Scope $scope, Node\Expr $iterateeExpr, ?Node\Exp ))->identifier('varTag.differentVariable')->build(); } - if ($this->checkTypeAgainstNativeType) { - 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) { + 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; } @@ -319,14 +321,12 @@ private function processStatic(Scope $scope, array $vars, array $varTags): array ))->identifier('varTag.differentVariable')->build(); } - if ($this->checkTypeAgainstNativeType) { - foreach ($vars as $var) { - if ($var->default === null) { - continue; - } - foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $var->var, $var->default, $varTags, $variableNames) as $error) { - $errors[] = $error; - } + foreach ($vars as $var) { + if ($var->default === null) { + continue; + } + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $var->var, $var->default, $varTags, $variableNames) as $error) { + $errors[] = $error; } } diff --git a/src/Rules/Playground/FunctionNeverRule.php b/src/Rules/Playground/FunctionNeverRule.php index 94171408dd..5bb4714521 100644 --- a/src/Rules/Playground/FunctionNeverRule.php +++ b/src/Rules/Playground/FunctionNeverRule.php @@ -5,7 +5,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\FunctionReturnStatementsNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function count; @@ -14,7 +13,7 @@ /** * @implements Rule */ -class FunctionNeverRule implements Rule +final class FunctionNeverRule implements Rule { public function __construct(private NeverRuleHelper $helper) @@ -34,7 +33,7 @@ public function processNode(Node $node, Scope $scope): array $function = $node->getFunctionReflection(); - $returnType = ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(); + $returnType = $function->getReturnType(); $helperResult = $this->helper->shouldReturnNever($node, $returnType); if ($helperResult === false) { return []; diff --git a/src/Rules/Playground/MethodNeverRule.php b/src/Rules/Playground/MethodNeverRule.php index 443a31112f..fdef4e9396 100644 --- a/src/Rules/Playground/MethodNeverRule.php +++ b/src/Rules/Playground/MethodNeverRule.php @@ -5,7 +5,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\MethodReturnStatementsNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function count; @@ -14,7 +13,7 @@ /** * @implements Rule */ -class MethodNeverRule implements Rule +final class MethodNeverRule implements Rule { public function __construct(private NeverRuleHelper $helper) @@ -34,7 +33,7 @@ public function processNode(Node $node, Scope $scope): array $method = $node->getMethodReflection(); - $returnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType(); + $returnType = $method->getReturnType(); $helperResult = $this->helper->shouldReturnNever($node, $returnType); if ($helperResult === false) { return []; diff --git a/src/Rules/Playground/NeverRuleHelper.php b/src/Rules/Playground/NeverRuleHelper.php index 9da99b88e9..a0870f7ce3 100644 --- a/src/Rules/Playground/NeverRuleHelper.php +++ b/src/Rules/Playground/NeverRuleHelper.php @@ -3,11 +3,13 @@ namespace PHPStan\Rules\Playground; use PhpParser\Node; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\ReturnStatementsNode; use PHPStan\Type\NeverType; use PHPStan\Type\Type; -class NeverRuleHelper +#[AutowiredService] +final class NeverRuleHelper { /** @@ -26,10 +28,17 @@ public function shouldReturnNever(ReturnStatementsNode $node, Type $returnType): $other = []; foreach ($node->getExecutionEnds() as $executionEnd) { if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { - if (!$executionEnd->getNode() instanceof Node\Stmt\Throw_) { + $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; } diff --git a/src/Rules/Playground/NoPhpCodeRule.php b/src/Rules/Playground/NoPhpCodeRule.php index 2042177a2c..c8d0646083 100644 --- a/src/Rules/Playground/NoPhpCodeRule.php +++ b/src/Rules/Playground/NoPhpCodeRule.php @@ -12,7 +12,7 @@ /** * @implements Rule */ -class NoPhpCodeRule implements Rule +final class NoPhpCodeRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Playground/NotAnalysedTraitRule.php b/src/Rules/Playground/NotAnalysedTraitRule.php index 7efa804442..c9ba6e0a24 100644 --- a/src/Rules/Playground/NotAnalysedTraitRule.php +++ b/src/Rules/Playground/NotAnalysedTraitRule.php @@ -15,7 +15,7 @@ /** * @implements Rule */ -class NotAnalysedTraitRule implements Rule +final class NotAnalysedTraitRule implements Rule { public function getNodeType(): string 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 d68fca010a..70efb0dab4 100644 --- a/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php +++ b/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php @@ -5,6 +5,7 @@ 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; @@ -12,7 +13,8 @@ /** * @implements Rule */ -class AccessPrivatePropertyThroughStaticRule implements Rule +#[RegisteredRule(level: 2)] +final class AccessPrivatePropertyThroughStaticRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Properties/AccessPropertiesCheck.php b/src/Rules/Properties/AccessPropertiesCheck.php new file mode 100644 index 0000000000..f24b5eddac --- /dev/null +++ b/src/Rules/Properties/AccessPropertiesCheck.php @@ -0,0 +1,255 @@ + + */ + 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->hasProperty($name)->yes(), + ); + $type = $typeResult->getType(); + if ($type instanceof ErrorType) { + return $typeResult->getUnknownClassErrors(); + } + + if ($scope->isInExpressionAssign($node)) { + return []; + } + + $typeForDescribe = $type; + if ($type instanceof StaticType) { + $typeForDescribe = $type->getStaticObjectType(); + } + + if ($type->canAccessProperties()->no() || $type->canAccessProperties()->maybe() && !$scope->isUndefinedExpressionAllowed($node)) { + return [ + RuleErrorBuilder::message(sprintf( + 'Cannot access property $%s on %s.', + $name, + $typeForDescribe->describe(VerbosityLevel::typeOnly()), + ))->identifier('property.nonObject')->build(), + ]; + } + + $has = $type->hasProperty($name); + if (!$has->no() && $this->canAccessUndefinedProperties($scope, $node)) { + return []; + } + + if (!$has->yes()) { + if ($scope->hasExpressionType($node)->yes()) { + return []; + } + + $classNames = $type->getObjectClassNames(); + if (!$this->reportMagicProperties) { + foreach ($classNames as $className) { + if (!$this->reflectionProvider->hasClass($className)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if ( + $classReflection->hasNativeMethod('__get') + || $classReflection->hasNativeMethod('__set') + ) { + return []; + } + } + } + + if (count($classNames) === 1) { + $propertyClassReflection = $this->reflectionProvider->getClass($classNames[0]); + $parentClassReflection = $propertyClassReflection->getParentClass(); + while ($parentClassReflection !== null) { + if ($parentClassReflection->hasProperty($name)) { + if ($write) { + if ($scope->canWriteProperty($parentClassReflection->getProperty($name, $scope))) { + return []; + } + } elseif ($scope->canReadProperty($parentClassReflection->getProperty($name, $scope))) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Access to private property $%s of parent class %s.', + $name, + $parentClassReflection->getDisplayName(), + ))->identifier('property.private')->build(), + ]; + } + + $parentClassReflection = $parentClassReflection->getParentClass(); + } + } + + if ($node->name instanceof Expr) { + $propertyExistsExpr = new FuncCall(new FullyQualified('property_exists'), [ + new Arg($node->var), + new Arg($node->name), + ]); + + if ($scope->getType($propertyExistsExpr)->isTrue()->yes()) { + return []; + } + } + + $ruleErrorBuilder = RuleErrorBuilder::message(sprintf( + 'Access to an undefined property %s::$%s.', + $typeForDescribe->describe(VerbosityLevel::typeOnly()), + $name, + ))->identifier('property.notFound'); + if ($typeResult->getTip() !== null) { + $ruleErrorBuilder->tip($typeResult->getTip()); + } else { + $ruleErrorBuilder->tip('Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'); + } + + return [ + $ruleErrorBuilder->build(), + ]; + } + + $propertyReflection = $type->getProperty($name, $scope); + if ($propertyReflection->isStatic()) { + return [ + RuleErrorBuilder::message(sprintf( + 'Non-static access to static property %s::$%s.', + $propertyReflection->getDeclaringClass()->getDisplayName(), + $name, + ))->identifier('staticProperty.nonStaticAccess')->build(), + ]; + } + + if ($write) { + if ($scope->canWriteProperty($propertyReflection)) { + return []; + } + } elseif ($scope->canReadProperty($propertyReflection)) { + return []; + } + + if ( + !$this->phpVersion->supportsAsymmetricVisibility() + || !$write + || (!$propertyReflection->isPrivateSet() && !$propertyReflection->isProtectedSet()) + ) { + return [ + RuleErrorBuilder::message(sprintf( + 'Access to %s property %s::$%s.', + $propertyReflection->isPrivate() ? 'private' : 'protected', + $type->describe(VerbosityLevel::typeOnly()), + $name, + ))->identifier(sprintf('property.%s', $propertyReflection->isPrivate() ? 'private' : 'protected'))->build(), + ]; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Assign to %s property %s::$%s.', + $propertyReflection->isPrivateSet() ? 'private(set)' : 'protected(set)', + $type->describe(VerbosityLevel::typeOnly()), + $name, + ))->identifier(sprintf('assign.property%s', $propertyReflection->isPrivateSet() ? 'PrivateSet' : 'ProtectedSet'))->build(), + ]; + } + + private function canAccessUndefinedProperties(Scope $scope, Expr $node): bool + { + return $scope->isUndefinedExpressionAllowed($node) && !$this->checkDynamicProperties; + } + +} diff --git a/src/Rules/Properties/AccessPropertiesInAssignRule.php b/src/Rules/Properties/AccessPropertiesInAssignRule.php index e5d3d08b4e..e53aff230e 100644 --- a/src/Rules/Properties/AccessPropertiesInAssignRule.php +++ b/src/Rules/Properties/AccessPropertiesInAssignRule.php @@ -4,16 +4,18 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\PropertyAssignNode; use PHPStan\Rules\Rule; /** * @implements Rule */ -class AccessPropertiesInAssignRule implements Rule +#[RegisteredRule(level: 0)] +final class AccessPropertiesInAssignRule implements Rule { - public function __construct(private AccessPropertiesRule $accessPropertiesRule) + public function __construct(private AccessPropertiesCheck $check) { } @@ -32,7 +34,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - return $this->accessPropertiesRule->processNode($node->getPropertyFetch(), $scope); + return $this->check->check($node->getPropertyFetch(), $scope, true); } } diff --git a/src/Rules/Properties/AccessPropertiesRule.php b/src/Rules/Properties/AccessPropertiesRule.php index 0a8d6cdbcf..27000383b0 100644 --- a/src/Rules/Properties/AccessPropertiesRule.php +++ b/src/Rules/Properties/AccessPropertiesRule.php @@ -4,37 +4,18 @@ use PhpParser\Node; use PhpParser\Node\Expr\PropertyFetch; -use PhpParser\Node\Identifier; -use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; -use PHPStan\Internal\SprintfHelper; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\IdentifierRuleError; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Rules\RuleLevelHelper; -use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ErrorType; -use PHPStan\Type\StaticType; -use PHPStan\Type\Type; -use PHPStan\Type\VerbosityLevel; -use function array_map; -use function array_merge; -use function count; -use function sprintf; /** * @implements Rule */ -class AccessPropertiesRule implements Rule +#[RegisteredRule(level: 0)] +final class AccessPropertiesRule implements Rule { - public function __construct( - private ReflectionProvider $reflectionProvider, - private RuleLevelHelper $ruleLevelHelper, - private bool $reportMagicProperties, - private bool $checkDynamicProperties, - ) + public function __construct(private AccessPropertiesCheck $check) { } @@ -45,137 +26,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if ($node->name instanceof Identifier) { - $names = [$node->name->name]; - } else { - $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $scope->getType($node->name)->getConstantStrings()); - } - - $errors = []; - foreach ($names as $name) { - $errors = array_merge($errors, $this->processSingleProperty($scope, $node, $name)); - } - - return $errors; - } - - /** - * @return list - */ - private function processSingleProperty(Scope $scope, PropertyFetch $node, string $name): array - { - $typeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var), - sprintf('Access to property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)), - static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes(), - ); - $type = $typeResult->getType(); - if ($type instanceof ErrorType) { - return $typeResult->getUnknownClassErrors(); - } - - if ($scope->isInExpressionAssign($node)) { - return []; - } - - $typeForDescribe = $type; - if ($type instanceof StaticType) { - $typeForDescribe = $type->getStaticObjectType(); - } - - if ($type->canAccessProperties()->no() || $type->canAccessProperties()->maybe() && !$scope->isUndefinedExpressionAllowed($node)) { - return [ - RuleErrorBuilder::message(sprintf( - 'Cannot access property $%s on %s.', - $name, - $typeForDescribe->describe(VerbosityLevel::typeOnly()), - ))->identifier('property.nonObject')->build(), - ]; - } - - $has = $type->hasProperty($name); - if (!$has->no() && $this->canAccessUndefinedProperties($scope, $node)) { - return []; - } - - if (!$has->yes()) { - if ($scope->hasExpressionType($node)->yes()) { - return []; - } - - $classNames = $type->getObjectClassNames(); - if (!$this->reportMagicProperties) { - foreach ($classNames as $className) { - if (!$this->reflectionProvider->hasClass($className)) { - continue; - } - - $classReflection = $this->reflectionProvider->getClass($className); - if ( - $classReflection->hasNativeMethod('__get') - || $classReflection->hasNativeMethod('__set') - ) { - return []; - } - } - } - - if (count($classNames) === 1) { - $propertyClassReflection = $this->reflectionProvider->getClass($classNames[0]); - $parentClassReflection = $propertyClassReflection->getParentClass(); - while ($parentClassReflection !== null) { - if ($parentClassReflection->hasProperty($name)) { - if ($scope->canAccessProperty($parentClassReflection->getProperty($name, $scope))) { - return []; - } - return [ - RuleErrorBuilder::message(sprintf( - 'Access to private property $%s of parent class %s.', - $name, - $parentClassReflection->getDisplayName(), - ))->identifier('property.private')->build(), - ]; - } - - $parentClassReflection = $parentClassReflection->getParentClass(); - } - } - - $ruleErrorBuilder = RuleErrorBuilder::message(sprintf( - 'Access to an undefined property %s::$%s.', - $typeForDescribe->describe(VerbosityLevel::typeOnly()), - $name, - ))->identifier('property.notFound'); - if ($typeResult->getTip() !== null) { - $ruleErrorBuilder->tip($typeResult->getTip()); - } else { - $ruleErrorBuilder->tip('Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'); - } - - return [ - $ruleErrorBuilder->build(), - ]; - } - - $propertyReflection = $type->getProperty($name, $scope); - if (!$scope->canAccessProperty($propertyReflection)) { - 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 []; - } - - private function canAccessUndefinedProperties(Scope $scope, Node\Expr $node): bool - { - return $scope->isUndefinedExpressionAllowed($node) && !$this->checkDynamicProperties; + return $this->check->check($node, $scope, false); } } diff --git a/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php b/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php index 21d6ff3e1f..71db3bb1e6 100644 --- a/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php @@ -4,13 +4,15 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\PropertyAssignNode; use PHPStan\Rules\Rule; /** * @implements Rule */ -class AccessStaticPropertiesInAssignRule implements Rule +#[RegisteredRule(level: 0)] +final class AccessStaticPropertiesInAssignRule implements Rule { public function __construct(private AccessStaticPropertiesRule $accessStaticPropertiesRule) diff --git a/src/Rules/Properties/AccessStaticPropertiesRule.php b/src/Rules/Properties/AccessStaticPropertiesRule.php index 9d929c39a2..2e52ae7202 100644 --- a/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -7,15 +7,17 @@ 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\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\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; use PHPStan\Type\StringType; @@ -34,13 +36,16 @@ /** * @implements Rule */ -class AccessStaticPropertiesRule implements Rule +#[RegisteredRule(level: 0)] +final class AccessStaticPropertiesRule implements Rule { public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, private ClassNameCheck $classCheck, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { } @@ -108,16 +113,6 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, ]; } - if ($scope->getFunctionName() === null) { - throw new ShouldNotHappenException(); - } - - $currentMethodReflection = $scope->getClassReflection()->getNativeMethod($scope->getFunctionName()); - if (!$currentMethodReflection->isStatic()) { - // calling parent::method() from instance method - return []; - } - $classType = $scope->resolveTypeByName($node->class); } else { if (!$this->reflectionProvider->hasClass($class)) { @@ -125,19 +120,33 @@ 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() - ->identifier('class.notFound') - ->build(), + $errorBuilder->build(), ]; } - $messages = $this->classCheck->checkClassNames([new ClassNameNodePair($class, $node->class)]); + $locationData = []; + $locationClassReflection = $this->reflectionProvider->getClass($class); + if ($locationClassReflection->hasProperty($name)) { + $locationData['property'] = $locationClassReflection->getProperty($name, $scope); + } + + $messages = $this->classCheck->checkClassNames( + $scope, + [new ClassNameNodePair($class, $node->class)], + ClassNameUsageLocation::from(ClassNameUsageLocation::STATIC_PROPERTY_ACCESS, $locationData), + ); $classType = $scope->resolveTypeByName($node->class); } @@ -195,7 +204,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, while ($parentClassReflection !== null) { if ($parentClassReflection->hasProperty($name)) { - if ($scope->canAccessProperty($parentClassReflection->getProperty($name, $scope))) { + if ($scope->canReadProperty($parentClassReflection->getProperty($name, $scope))) { return []; } return [ @@ -238,7 +247,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, ]); } - if (!$scope->canAccessProperty($property)) { + if (!$scope->canReadProperty($property)) { return array_merge($messages, [ RuleErrorBuilder::message(sprintf( 'Access to %s property $%s of class %s.', diff --git a/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php b/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php index 8fd67c1e5f..ed947b1a67 100644 --- a/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php +++ b/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php @@ -4,18 +4,19 @@ 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 Rule */ -class DefaultValueTypesAssignedToPropertiesRule implements Rule +#[RegisteredRule(level: 3)] +final class DefaultValueTypesAssignedToPropertiesRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) @@ -38,13 +39,13 @@ public function processNode(Node $node, Scope $scope): array $propertyReflection = $classReflection->getNativeProperty($node->getName()); $propertyType = $propertyReflection->getWritableType(); - if ($propertyReflection->getNativeType() instanceof MixedType) { + if (!$propertyReflection->hasNativeType()) { if ($default instanceof Node\Expr\ConstFetch && $default->name->toLowerString() === 'null') { return []; } } $defaultValueType = $scope->getType($default); - $accepts = $this->ruleLevelHelper->acceptsWithReason($propertyType, $defaultValueType, true); + $accepts = $this->ruleLevelHelper->accepts($propertyType, $defaultValueType, true); if ($accepts->result) { return []; } diff --git a/src/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php b/src/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php index 6ce75189d7..0130c2f633 100644 --- a/src/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php +++ b/src/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php @@ -2,7 +2,7 @@ namespace PHPStan\Rules\Properties; -class DirectReadWritePropertiesExtensionProvider implements ReadWritePropertiesExtensionProvider +final class DirectReadWritePropertiesExtensionProvider implements ReadWritePropertiesExtensionProvider { /** diff --git a/src/Rules/Properties/ExistingClassesInPropertiesRule.php b/src/Rules/Properties/ExistingClassesInPropertiesRule.php index 2b138d5bbd..ae9a7af28b 100644 --- a/src/Rules/Properties/ExistingClassesInPropertiesRule.php +++ b/src/Rules/Properties/ExistingClassesInPropertiesRule.php @@ -4,11 +4,14 @@ 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\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -19,7 +22,8 @@ /** * @implements Rule */ -class ExistingClassesInPropertiesRule implements Rule +#[RegisteredRule(level: 0)] +final class ExistingClassesInPropertiesRule implements Rule { public function __construct( @@ -27,8 +31,12 @@ public function __construct( private ClassNameCheck $classCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, private PhpVersion $phpVersion, + #[AutowiredParameter] private bool $checkClassCaseSensitivity, + #[AutowiredParameter] private bool $checkThisOnly, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { } @@ -64,18 +72,29 @@ public function processNode(Node $node, Scope $scope): array 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, - ))->identifier('class.notFound')->discoveringSymbolsTip()->build(); + )) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); } $errors = array_merge( $errors, $this->classCheck->checkClassNames( + $scope, array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $node), $referencedClasses), + ClassNameUsageLocation::from(ClassNameUsageLocation::PROPERTY_TYPE, [ + 'property' => $propertyReflection, + ]), $this->checkClassCaseSensitivity, ), ); diff --git a/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php b/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php new file mode 100644 index 0000000000..3530ee5769 --- /dev/null +++ b/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php @@ -0,0 +1,90 @@ + + */ +#[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, + ), + ); + } + +} diff --git a/src/Rules/Properties/FoundPropertyReflection.php b/src/Rules/Properties/FoundPropertyReflection.php index 0132ac0d3b..b1a890b6be 100644 --- a/src/Rules/Properties/FoundPropertyReflection.php +++ b/src/Rules/Properties/FoundPropertyReflection.php @@ -4,17 +4,18 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Php\PhpPropertyReflection; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\WrapperPropertyReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -class FoundPropertyReflection implements PropertyReflection +final class FoundPropertyReflection implements ExtendedPropertyReflection { public function __construct( - private PropertyReflection $originalPropertyReflection, + private ExtendedPropertyReflection $originalPropertyReflection, private Scope $scope, private string $propertyName, private Type $readableType, @@ -58,6 +59,26 @@ public function getDocComment(): ?string return $this->originalPropertyReflection->getDocComment(); } + public function hasPhpDocType(): bool + { + return $this->originalPropertyReflection->hasPhpDocType(); + } + + public function getPhpDocType(): Type + { + return $this->originalPropertyReflection->getPhpDocType(); + } + + public function hasNativeType(): bool + { + return $this->originalPropertyReflection->hasNativeType(); + } + + public function getNativeType(): Type + { + return $this->originalPropertyReflection->getNativeType(); + } + public function getReadableType(): Type { return $this->readableType; @@ -103,16 +124,6 @@ public function isNative(): bool return $this->getNativeReflection() !== null; } - public function getNativeType(): ?Type - { - $reflection = $this->getNativeReflection(); - if ($reflection === null) { - return null; - } - - return $reflection->getNativeType(); - } - public function getNativeReflection(): ?PhpPropertyReflection { $reflection = $this->originalPropertyReflection; @@ -127,4 +138,49 @@ public function getNativeReflection(): ?PhpPropertyReflection 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(); + } + } 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 index 2a8ed41bd8..e0fe89a5b3 100644 --- a/src/Rules/Properties/InvalidCallablePropertyTypeRule.php +++ b/src/Rules/Properties/InvalidCallablePropertyTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClassPropertyNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -17,7 +18,8 @@ /** * @implements Rule */ -class InvalidCallablePropertyTypeRule implements Rule +#[RegisteredRule(level: 0)] +final class InvalidCallablePropertyTypeRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Properties/LazyReadWritePropertiesExtensionProvider.php b/src/Rules/Properties/LazyReadWritePropertiesExtensionProvider.php index 3f61fd0fb8..603c685105 100644 --- a/src/Rules/Properties/LazyReadWritePropertiesExtensionProvider.php +++ b/src/Rules/Properties/LazyReadWritePropertiesExtensionProvider.php @@ -2,9 +2,11 @@ namespace PHPStan\Rules\Properties; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; -class LazyReadWritePropertiesExtensionProvider implements ReadWritePropertiesExtensionProvider +#[AutowiredService] +final class LazyReadWritePropertiesExtensionProvider implements ReadWritePropertiesExtensionProvider { /** @var ReadWritePropertiesExtension[]|null */ @@ -16,11 +18,7 @@ public function __construct(private 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 c429a30fdd..450b6af0ea 100644 --- a/src/Rules/Properties/MissingPropertyTypehintRule.php +++ b/src/Rules/Properties/MissingPropertyTypehintRule.php @@ -4,18 +4,19 @@ 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 implode; use function sprintf; /** * @implements Rule */ +#[RegisteredRule(level: 6)] final class MissingPropertyTypehintRule implements Rule { @@ -68,7 +69,7 @@ public function processNode(Node $node, Scope $scope): array $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php b/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php index 72b30052e6..35bedec527 100644 --- a/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php +++ b/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClassPropertiesNode; use PHPStan\Reflection\ConstructorsHelper; use PHPStan\Rules\Rule; @@ -13,7 +14,8 @@ /** * @implements Rule */ -class MissingReadOnlyByPhpDocPropertyAssignRule implements Rule +#[RegisteredRule(level: 0)] +final class MissingReadOnlyByPhpDocPropertyAssignRule implements Rule { public function __construct( diff --git a/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php b/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php index 54d64878ad..1f2c1ff721 100644 --- a/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php +++ b/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClassPropertiesNode; use PHPStan\Reflection\ConstructorsHelper; use PHPStan\Rules\Rule; @@ -13,7 +14,8 @@ /** * @implements Rule */ -class MissingReadOnlyPropertyAssignRule implements Rule +#[RegisteredRule(level: 0)] +final class MissingReadOnlyPropertyAssignRule implements Rule { public function __construct( diff --git a/src/Rules/Properties/NullsafePropertyFetchRule.php b/src/Rules/Properties/NullsafePropertyFetchRule.php index 1195f47022..5348cb351d 100644 --- a/src/Rules/Properties/NullsafePropertyFetchRule.php +++ b/src/Rules/Properties/NullsafePropertyFetchRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; @@ -12,7 +13,8 @@ /** * @implements Rule */ -class NullsafePropertyFetchRule implements Rule +#[RegisteredRule(level: 4)] +final class NullsafePropertyFetchRule implements Rule { public function __construct() diff --git a/src/Rules/Properties/OverridingPropertyRule.php b/src/Rules/Properties/OverridingPropertyRule.php index ca10cd4434..0def12649a 100644 --- a/src/Rules/Properties/OverridingPropertyRule.php +++ b/src/Rules/Properties/OverridingPropertyRule.php @@ -4,12 +4,14 @@ 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; @@ -18,11 +20,15 @@ /** * @implements Rule */ -class OverridingPropertyRule implements Rule +#[RegisteredRule(level: 0)] +final class OverridingPropertyRule implements Rule { public function __construct( + private PhpVersion $phpVersion, + #[AutowiredParameter] private bool $checkPhpDocMethodSignatures, + #[AutowiredParameter(ref: '%reportMaybesInPropertyPhpDocTypes%')] private bool $reportMaybes, ) { @@ -73,13 +79,44 @@ public function processNode(Node $node, Scope $scope): array ))->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(), - ))->identifier('property.readOnly')->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()) { @@ -103,9 +140,31 @@ public function processNode(Node $node, Scope $scope): array ))->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(), @@ -116,25 +175,54 @@ public function processNode(Node $node, Scope $scope): array $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), ))->identifier('property.missingNativeType')->nonIgnorable()->build(); } else { - $nativeType = ParserNodeTypeToPHPStanType::resolve($node->getNativeType(), $classReflection); if (!$prototype->getNativeType()->equals($nativeType)) { - $typeErrors[] = RuleErrorBuilder::message(sprintf( - 'Type %s of property %s::$%s is not the same as type %s of overridden property %s::$%s.', - $nativeType->describe(VerbosityLevel::typeOnly()), - $classReflection->getDisplayName(), - $node->getName(), - $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), - $node->getName(), - ))->identifier('property.nativeType')->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(), $classReflection)->describe(VerbosityLevel::typeOnly()), + $nativeType->describe(VerbosityLevel::typeOnly()), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), ))->identifier('property.extraNativeType')->nonIgnorable()->build(); @@ -150,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.', @@ -202,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 index 35ce4c2c8c..223623f069 100644 --- a/src/Rules/Properties/PropertiesInInterfaceRule.php +++ b/src/Rules/Properties/PropertiesInInterfaceRule.php @@ -4,16 +4,23 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClassPropertyNode; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; /** * @implements Rule */ -class PropertiesInInterfaceRule implements Rule +#[RegisteredRule(level: 0)] +final class PropertiesInInterfaceRule implements Rule { + public function __construct(private PhpVersion $phpVersion) + { + } + public function getNodeType(): string { return ClassPropertyNode::class; @@ -25,12 +32,103 @@ public function processNode(Node $node, Scope $scope): array return []; } - return [ - RuleErrorBuilder::message('Interfaces may not include properties.') - ->nonIgnorable() - ->identifier('property.inInterface') - ->build(), - ]; + 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 b0d9cf2812..2a77c1e01a 100644 --- a/src/Rules/Properties/PropertyAttributesRule.php +++ b/src/Rules/Properties/PropertyAttributesRule.php @@ -5,13 +5,15 @@ 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 { public function __construct(private AttributesCheck $attributesCheck) diff --git a/src/Rules/Properties/PropertyDescriptor.php b/src/Rules/Properties/PropertyDescriptor.php index e45011b361..d9d6e10c99 100644 --- a/src/Rules/Properties/PropertyDescriptor.php +++ b/src/Rules/Properties/PropertyDescriptor.php @@ -4,12 +4,14 @@ 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 { /** diff --git a/src/Rules/Properties/PropertyHookAttributesRule.php b/src/Rules/Properties/PropertyHookAttributesRule.php new file mode 100644 index 0000000000..509fe8f83e --- /dev/null +++ b/src/Rules/Properties/PropertyHookAttributesRule.php @@ -0,0 +1,39 @@ + + */ +#[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 + { + return $this->attributesCheck->check( + $scope, + $node->getOriginalNode()->attrGroups, + Attribute::TARGET_METHOD, + 'method', + ); + } + +} diff --git a/src/Rules/Properties/PropertyInClassRule.php b/src/Rules/Properties/PropertyInClassRule.php new file mode 100644 index 0000000000..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 3f6cc8b000..3daa5ee867 100644 --- a/src/Rules/Properties/PropertyReflectionFinder.php +++ b/src/Rules/Properties/PropertyReflectionFinder.php @@ -7,11 +7,14 @@ 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 function array_map; +use function count; -class PropertyReflectionFinder +#[AutowiredService] +final class PropertyReflectionFinder { /** @@ -86,11 +89,18 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a public function findPropertyReflectionFromNode($propertyFetch, Scope $scope): ?FoundPropertyReflection { if ($propertyFetch instanceof Node\Expr\PropertyFetch) { - if (!$propertyFetch->name instanceof Node\Identifier) { - return null; - } $propertyHolderType = $scope->getType($propertyFetch->var); - return $this->findPropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); + if ($propertyFetch->name instanceof Node\Identifier) { + return $this->findPropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); + } + + $nameType = $scope->getType($propertyFetch->name); + $nameTypeConstantStrings = $nameType->getConstantStrings(); + if (count($nameTypeConstantStrings) === 1) { + return $this->findPropertyReflection($propertyHolderType, $nameTypeConstantStrings[0]->getValue(), $scope); + } + + return null; } if (!$propertyFetch->name instanceof Node\Identifier) { diff --git a/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php index ce0b1d1f05..3f0911fe37 100644 --- a/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php +++ b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; @@ -11,7 +12,8 @@ /** * @implements Rule */ -class ReadOnlyByPhpDocPropertyAssignRefRule implements Rule +#[RegisteredRule(level: 3)] +final class ReadOnlyByPhpDocPropertyAssignRefRule implements Rule { public function __construct(private PropertyReflectionFinder $propertyReflectionFinder) @@ -38,7 +40,7 @@ public function processNode(Node $node, Scope $scope): array if ($nativeReflection === null) { continue; } - if (!$scope->canAccessProperty($propertyReflection)) { + if (!$scope->canWriteProperty($propertyReflection)) { continue; } if (!$nativeReflection->isReadOnlyByPhpDoc() || $nativeReflection->isReadOnly()) { diff --git a/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php index 9d72bb3895..4f2eb63a76 100644 --- a/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php +++ b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php @@ -2,14 +2,20 @@ namespace PHPStan\Rules\Properties; +use ArrayAccess; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\Expr\SetOffsetValueTypeExpr; +use PHPStan\Node\Expr\UnsetOffsetExpr; use PHPStan\Node\PropertyAssignNode; use PHPStan\Reflection\ConstructorsHelper; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\ObjectType; use PHPStan\Type\TypeUtils; use function in_array; use function sprintf; @@ -18,7 +24,8 @@ /** * @implements Rule */ -class ReadOnlyByPhpDocPropertyAssignRule implements Rule +#[RegisteredRule(level: 3)] +final class ReadOnlyByPhpDocPropertyAssignRule implements Rule { public function __construct( @@ -40,6 +47,18 @@ public function processNode(Node $node, Scope $scope): array 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) { @@ -47,7 +66,7 @@ public function processNode(Node $node, Scope $scope): array if ($nativeReflection === null) { continue; } - if (!$scope->canAccessProperty($propertyReflection)) { + if (!$scope->canWriteProperty($propertyReflection)) { continue; } if (!$nativeReflection->isReadOnlyByPhpDoc() || $nativeReflection->isReadOnly()) { @@ -93,6 +112,14 @@ public function processNode(Node $node, Scope $scope): array 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(); diff --git a/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php b/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php index a024d70b14..f45c6a701e 100644 --- a/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php +++ b/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClassPropertyNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -11,7 +12,8 @@ /** * @implements Rule */ -class ReadOnlyByPhpDocPropertyRule implements Rule +#[RegisteredRule(level: 0)] +final class ReadOnlyByPhpDocPropertyRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/Properties/ReadOnlyPropertyAssignRefRule.php b/src/Rules/Properties/ReadOnlyPropertyAssignRefRule.php index b50e887e3b..08a5db774b 100644 --- a/src/Rules/Properties/ReadOnlyPropertyAssignRefRule.php +++ b/src/Rules/Properties/ReadOnlyPropertyAssignRefRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; @@ -11,7 +12,8 @@ /** * @implements Rule */ -class ReadOnlyPropertyAssignRefRule implements Rule +#[RegisteredRule(level: 3)] +final class ReadOnlyPropertyAssignRefRule implements Rule { public function __construct(private PropertyReflectionFinder $propertyReflectionFinder) @@ -25,7 +27,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->expr instanceof Node\Expr\PropertyFetch && !$node->expr instanceof Node\Expr\StaticPropertyFetch) { + if (!$node->expr instanceof Node\Expr\PropertyFetch) { return []; } @@ -38,7 +40,7 @@ public function processNode(Node $node, Scope $scope): array if ($nativeReflection === null) { continue; } - if (!$scope->canAccessProperty($propertyReflection)) { + if (!$scope->canWriteProperty($propertyReflection)) { continue; } if (!$nativeReflection->isReadOnly()) { diff --git a/src/Rules/Properties/ReadOnlyPropertyAssignRule.php b/src/Rules/Properties/ReadOnlyPropertyAssignRule.php index 47d132b639..75e52f49d8 100644 --- a/src/Rules/Properties/ReadOnlyPropertyAssignRule.php +++ b/src/Rules/Properties/ReadOnlyPropertyAssignRule.php @@ -2,14 +2,19 @@ namespace PHPStan\Rules\Properties; +use ArrayAccess; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\Expr\SetOffsetValueTypeExpr; +use PHPStan\Node\Expr\UnsetOffsetExpr; use PHPStan\Node\PropertyAssignNode; use PHPStan\Reflection\ConstructorsHelper; use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\ObjectType; use PHPStan\Type\TypeUtils; use function in_array; use function sprintf; @@ -18,7 +23,8 @@ /** * @implements Rule */ -class ReadOnlyPropertyAssignRule implements Rule +#[RegisteredRule(level: 3)] +final class ReadOnlyPropertyAssignRule implements Rule { public function __construct( @@ -47,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array if ($nativeReflection === null) { continue; } - if (!$scope->canAccessProperty($propertyReflection)) { + if (!$scope->canWriteProperty($propertyReflection)) { continue; } if (!$nativeReflection->isReadOnly()) { @@ -89,6 +95,14 @@ public function processNode(Node $node, Scope $scope): array 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(); diff --git a/src/Rules/Properties/ReadOnlyPropertyRule.php b/src/Rules/Properties/ReadOnlyPropertyRule.php index a622964ed5..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,7 +13,8 @@ /** * @implements Rule */ -class ReadOnlyPropertyRule implements Rule +#[RegisteredRule(level: 0)] +final class ReadOnlyPropertyRule implements Rule { public function __construct(private PhpVersion $phpVersion) diff --git a/src/Rules/Properties/ReadWritePropertiesExtension.php b/src/Rules/Properties/ReadWritePropertiesExtension.php index 1b1c695ef9..804619781c 100644 --- a/src/Rules/Properties/ReadWritePropertiesExtension.php +++ b/src/Rules/Properties/ReadWritePropertiesExtension.php @@ -2,7 +2,7 @@ namespace PHPStan\Rules\Properties; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; /** * This is the extension interface to implement if you want to describe @@ -25,10 +25,10 @@ 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 5e326c12a8..daecd99b68 100644 --- a/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php +++ b/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php @@ -4,6 +4,8 @@ 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; @@ -12,13 +14,15 @@ /** * @implements Rule */ -class ReadingWriteOnlyPropertiesRule implements Rule +#[RegisteredRule(level: 0)] +final class ReadingWriteOnlyPropertiesRule implements Rule { public function __construct( private PropertyDescriptor $propertyDescriptor, private PropertyReflectionFinder $propertyReflectionFinder, private RuleLevelHelper $ruleLevelHelper, + #[AutowiredParameter] private bool $checkThisOnly, ) { @@ -54,7 +58,7 @@ public function processNode(Node $node, Scope $scope): array if ($propertyReflection === null) { return []; } - if (!$scope->canAccessProperty($propertyReflection)) { + if (!$scope->canReadProperty($propertyReflection)) { return []; } 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 9e15b0b33f..472ce3568e 100644 --- a/src/Rules/Properties/TypesAssignedToPropertiesRule.php +++ b/src/Rules/Properties/TypesAssignedToPropertiesRule.php @@ -3,8 +3,12 @@ namespace PHPStan\Rules\Properties; use PhpParser\Node; +use PhpParser\Node\Expr\PropertyFetch; +use PhpParser\Node\Expr\StaticPropertyFetch; use PHPStan\Analyser\Scope; +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; @@ -12,12 +16,14 @@ use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\VerbosityLevel; use function array_merge; +use function is_string; use function sprintf; /** * @implements Rule */ -class TypesAssignedToPropertiesRule implements Rule +#[RegisteredRule(level: 3)] +final class TypesAssignedToPropertiesRule implements Rule { public function __construct( @@ -34,12 +40,14 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $propertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($node->getPropertyFetch(), $scope); + $propertyFetch = $node->getPropertyFetch(); + $propertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($propertyFetch, $scope); $errors = []; foreach ($propertyReflections as $propertyReflection) { $errors = array_merge($errors, $this->processSingleProperty( $propertyReflection, + $propertyFetch, $node->getAssignedExpr(), )); } @@ -52,6 +60,7 @@ public function processNode(Node $node, Scope $scope): array */ private function processSingleProperty( FoundPropertyReflection $propertyReflection, + PropertyFetch|StaticPropertyFetch $fetch, Node\Expr $assignedExpr, ): array { @@ -59,11 +68,26 @@ private function processSingleProperty( return []; } - $propertyType = $propertyReflection->getWritableType(); $scope = $propertyReflection->getScope(); + $inFunction = $scope->getFunction(); + if ( + $fetch instanceof PropertyFetch + && $fetch->var instanceof Node\Expr\Variable + && is_string($fetch->var->name) + && $fetch->var->name === 'this' + && $fetch->name instanceof Node\Identifier + && $inFunction instanceof PhpMethodFromParserNodeReflection + && $inFunction->isPropertyHook() + && $inFunction->getHookedPropertyName() === $fetch->name->toString() + ) { + $propertyType = $propertyReflection->getReadableType(); + } else { + $propertyType = $propertyReflection->getWritableType(); + } + $assignedValueType = $scope->getType($assignedExpr); - $accepts = $this->ruleLevelHelper->acceptsWithReason($propertyType, $assignedValueType, $scope->isDeclareStrictTypes()); + $accepts = $this->ruleLevelHelper->accepts($propertyType, $assignedValueType, $scope->isDeclareStrictTypes()); if (!$accepts->result) { $propertyDescription = $this->describePropertyByName($propertyReflection, $propertyReflection->getName()); $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($propertyType, $assignedValueType); diff --git a/src/Rules/Properties/UninitializedPropertyRule.php b/src/Rules/Properties/UninitializedPropertyRule.php index a2995c3886..525a9ecbb6 100644 --- a/src/Rules/Properties/UninitializedPropertyRule.php +++ b/src/Rules/Properties/UninitializedPropertyRule.php @@ -13,7 +13,7 @@ /** * @implements Rule */ -class UninitializedPropertyRule implements Rule +final class UninitializedPropertyRule implements Rule { public function __construct( diff --git a/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php b/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php index 5acc86ba91..8ac259be31 100644 --- a/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php +++ b/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php @@ -4,6 +4,8 @@ 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; @@ -13,13 +15,15 @@ /** * @implements Rule */ -class WritingToReadOnlyPropertiesRule implements Rule +#[RegisteredRule(level: 0)] +final class WritingToReadOnlyPropertiesRule implements Rule { public function __construct( private RuleLevelHelper $ruleLevelHelper, private PropertyDescriptor $propertyDescriptor, private PropertyReflectionFinder $propertyReflectionFinder, + #[AutowiredParameter] private bool $checkThisOnly, ) { @@ -46,7 +50,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (!$scope->canAccessProperty($propertyReflection)) { + if (!$scope->canWriteProperty($propertyReflection)) { return []; } diff --git a/src/Rules/Pure/FunctionPurityCheck.php b/src/Rules/Pure/FunctionPurityCheck.php index 4e294dc0fe..1e5f7a32d6 100644 --- a/src/Rules/Pure/FunctionPurityCheck.php +++ b/src/Rules/Pure/FunctionPurityCheck.php @@ -7,9 +7,10 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\ThrowPoint; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Rules\Functions\CallToFunctionStatementWithoutSideEffectsRule; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; @@ -20,12 +21,13 @@ use function lcfirst; use function sprintf; -class FunctionPurityCheck +#[AutowiredService] +final class FunctionPurityCheck { /** * @param 'Function'|'Method' $identifier - * @param ParameterReflectionWithPhpDocs[] $parameters + * @param ExtendedParameterReflection[] $parameters * @param ImpurePoint[] $impurePoints * @param ThrowPoint[] $throwPoints * @param Stmt[] $statements @@ -40,18 +42,11 @@ public function check( array $impurePoints, array $throwPoints, array $statements, + bool $isConstructor, ): array { $errors = []; $isPure = $functionReflection->isPure(); - $isConstructor = false; - if ( - $functionReflection instanceof ExtendedMethodReflection - && $functionReflection->getDeclaringClass()->hasConstructor() - && $functionReflection->getDeclaringClass()->getConstructor()->getName() === $functionReflection->getName() - ) { - $isConstructor = true; - } if ($isPure->yes()) { foreach ($parameters as $parameter) { @@ -66,7 +61,13 @@ public function check( ))->identifier(sprintf('pure%s.parameterByRef', $identifier))->build(); } - if ($returnType->isVoid()->yes() && !$isConstructor) { + $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, @@ -93,6 +94,11 @@ public function check( 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.', diff --git a/src/Rules/Pure/PureFunctionRule.php b/src/Rules/Pure/PureFunctionRule.php index 838e9bc67b..56ec8aa269 100644 --- a/src/Rules/Pure/PureFunctionRule.php +++ b/src/Rules/Pure/PureFunctionRule.php @@ -4,15 +4,16 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\FunctionReturnStatementsNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use function sprintf; /** * @implements Rule */ -class PureFunctionRule implements Rule +#[RegisteredRule(level: 2)] +final class PureFunctionRule implements Rule { public function __construct(private FunctionPurityCheck $check) @@ -27,17 +28,17 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $function = $node->getFunctionReflection(); - $variant = ParametersAcceptorSelector::selectSingle($function->getVariants()); return $this->check->check( sprintf('Function %s()', $function->getName()), 'Function', $function, - $variant->getParameters(), - $variant->getReturnType(), + $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 index a023720118..9bb11a7e6f 100644 --- a/src/Rules/Pure/PureMethodRule.php +++ b/src/Rules/Pure/PureMethodRule.php @@ -4,15 +4,16 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\MethodReturnStatementsNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use function sprintf; /** * @implements Rule */ -class PureMethodRule implements Rule +#[RegisteredRule(level: 2)] +final class PureMethodRule implements Rule { public function __construct(private FunctionPurityCheck $check) @@ -27,17 +28,17 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $method = $node->getMethodReflection(); - $variant = ParametersAcceptorSelector::selectSingle($method->getVariants()); return $this->check->check( sprintf('Method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()), 'Method', $method, - $variant->getParameters(), - $variant->getReturnType(), + $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 fde058e62a..b3a1f98d5d 100644 --- a/src/Rules/Regexp/RegularExpressionPatternRule.php +++ b/src/Rules/Regexp/RegularExpressionPatternRule.php @@ -7,6 +7,7 @@ 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\Regex\RegexExpressionHelper; @@ -18,7 +19,8 @@ /** * @implements Rule */ -class RegularExpressionPatternRule implements Rule +#[RegisteredRule(level: 0)] +final class RegularExpressionPatternRule implements Rule { public function __construct( diff --git a/src/Rules/Regexp/RegularExpressionQuotingRule.php b/src/Rules/Regexp/RegularExpressionQuotingRule.php index e6a5839fdb..210b8d7518 100644 --- a/src/Rules/Regexp/RegularExpressionQuotingRule.php +++ b/src/Rules/Regexp/RegularExpressionQuotingRule.php @@ -8,6 +8,7 @@ use PhpParser\Node\Name; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; @@ -27,7 +28,8 @@ /** * @implements Rule */ -class RegularExpressionQuotingRule implements Rule +#[RegisteredRule(level: 5)] +final class RegularExpressionQuotingRule implements Rule { public function __construct( diff --git a/src/Rules/Registry.php b/src/Rules/Registry.php index 36c609811b..792f4428f0 100644 --- a/src/Rules/Registry.php +++ b/src/Rules/Registry.php @@ -9,10 +9,8 @@ interface Registry /** * @template TNodeType of Node - * @phpstan-param class-string $nodeType - * @param Node $nodeType - * @phpstan-return array> - * @return Rule[] + * @param class-string $nodeType + * @return array> */ public function getRules(string $nodeType): array; 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->hasProperty($propertyName)) { + continue; + } + + $propertyReflection = $classReflection->getProperty($propertyName, $scope); + foreach ($extensions as $extension) { + $restrictedUsage = $extension->isRestrictedPropertyUsage($propertyReflection, $scope); + if ($restrictedUsage === null) { + continue; + } + + $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier) + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php new file mode 100644 index 0000000000..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..5d45cb56a6 --- /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->hasProperty($propertyName)->yes(), + ); + + if ($classTypeResult->getType() instanceof ErrorType) { + return []; + } + + $referencedClasses = $classTypeResult->getReferencedClasses(); + } + + $errors = []; + foreach ($referencedClasses as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($referencedClass); + if (!$classReflection->hasProperty($propertyName)) { + continue; + } + + $propertyReflection = $classReflection->getProperty($propertyName, $scope); + foreach ($extensions as $extension) { + $restrictedUsage = $extension->isRestrictedPropertyUsage($propertyReflection, $scope); + if ($restrictedUsage === null) { + continue; + } + + 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..a2575177a5 --- /dev/null +++ b/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php @@ -0,0 +1,148 @@ +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(); + } + +} diff --git a/src/Rules/RestrictedUsage/RewrittenDeclaringClassPropertyReflection.php b/src/Rules/RestrictedUsage/RewrittenDeclaringClassPropertyReflection.php new file mode 100644 index 0000000000..d6b0424be9 --- /dev/null +++ b/src/Rules/RestrictedUsage/RewrittenDeclaringClassPropertyReflection.php @@ -0,0 +1,156 @@ +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(); + } + +} diff --git a/src/Rules/Rule.php b/src/Rules/Rule.php index e145e4b530..e42e6ccc52 100644 --- a/src/Rules/Rule.php +++ b/src/Rules/Rule.php @@ -20,19 +20,19 @@ * Learn more: https://phpstan.org/developing-extensions/rules * * @api - * @phpstan-template TNodeType of Node + * @template TNodeType of Node */ interface Rule { /** - * @phpstan-return class-string + * @return class-string */ public function getNodeType(): string; /** - * @phpstan-param TNodeType $node - * @return (string|RuleError)[] errors + * @param TNodeType $node + * @return list */ public function processNode(Node $node, Scope $scope): array; diff --git a/src/Rules/RuleErrorBuilder.php b/src/Rules/RuleErrorBuilder.php index a838200800..ec48db9cf1 100644 --- a/src/Rules/RuleErrorBuilder.php +++ b/src/Rules/RuleErrorBuilder.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules; +use PhpParser\Node; use PHPStan\Analyser\Error; use PHPStan\ShouldNotHappenException; use function array_map; @@ -15,7 +16,7 @@ * @api * @template-covariant T of RuleError */ -class RuleErrorBuilder +final class RuleErrorBuilder { private const TYPE_MESSAGE = 1; @@ -25,6 +26,7 @@ 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; @@ -50,9 +52,9 @@ public static function getRuleErrorTypes(): array RuleError::class, [ [ - 'message', - 'string', - 'string', + 'message', // property name + 'string', // native type + 'string', // PHPDoc type ], ], ], @@ -115,6 +117,21 @@ public static function getRuleErrorTypes(): array NonIgnorableRuleError::class, [], ], + self::TYPE_FIXABLE_NODE => [ + FixableNodeRuleError::class, + [ + [ + 'originalNode', + '\PhpParser\Node', + '\PhpParser\Node', + ], + [ + 'newNodeCallable', + null, + 'callable(\PhpParser\Node): \PhpParser\Node', + ], + ], + ], ]; } @@ -254,6 +271,23 @@ 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 */ diff --git a/src/Rules/RuleErrors/RuleError1.php b/src/Rules/RuleErrors/RuleError1.php index ef7771dea3..c7a2338b30 100644 --- a/src/Rules/RuleErrors/RuleError1.php +++ b/src/Rules/RuleErrors/RuleError1.php @@ -7,7 +7,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError1 implements RuleError +final class RuleError1 implements RuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError101.php b/src/Rules/RuleErrors/RuleError101.php index cd6a40afe3..ff16ad5339 100644 --- a/src/Rules/RuleErrors/RuleError101.php +++ b/src/Rules/RuleErrors/RuleError101.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError101 implements RuleError, FileRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError101 implements RuleError, FileRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError103.php b/src/Rules/RuleErrors/RuleError103.php index a88780c212..6c125de9b7 100644 --- a/src/Rules/RuleErrors/RuleError103.php +++ b/src/Rules/RuleErrors/RuleError103.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError103 implements RuleError, LineRuleError, FileRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError103 implements RuleError, LineRuleError, FileRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError105.php b/src/Rules/RuleErrors/RuleError105.php index 0fb7b8bc41..a0b8945f52 100644 --- a/src/Rules/RuleErrors/RuleError105.php +++ b/src/Rules/RuleErrors/RuleError105.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError105 implements RuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError105 implements RuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError107.php b/src/Rules/RuleErrors/RuleError107.php index 35b081c092..a0b9b85c84 100644 --- a/src/Rules/RuleErrors/RuleError107.php +++ b/src/Rules/RuleErrors/RuleError107.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError107 implements RuleError, LineRuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError107 implements RuleError, LineRuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError109.php b/src/Rules/RuleErrors/RuleError109.php index 22ddff6f25..a4f81cce53 100644 --- a/src/Rules/RuleErrors/RuleError109.php +++ b/src/Rules/RuleErrors/RuleError109.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError109 implements RuleError, FileRuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError109 implements RuleError, FileRuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError11.php b/src/Rules/RuleErrors/RuleError11.php index 50d8bdb997..be6bc0923a 100644 --- a/src/Rules/RuleErrors/RuleError11.php +++ b/src/Rules/RuleErrors/RuleError11.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError11 implements RuleError, LineRuleError, TipRuleError +final class RuleError11 implements RuleError, LineRuleError, TipRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError111.php b/src/Rules/RuleErrors/RuleError111.php index 3024d5fdf6..ac0b980e01 100644 --- a/src/Rules/RuleErrors/RuleError111.php +++ b/src/Rules/RuleErrors/RuleError111.php @@ -12,7 +12,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError111 implements RuleError, LineRuleError, FileRuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError111 implements RuleError, LineRuleError, FileRuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError113.php b/src/Rules/RuleErrors/RuleError113.php index ee74c1f0ac..5d602a2fe5 100644 --- a/src/Rules/RuleErrors/RuleError113.php +++ b/src/Rules/RuleErrors/RuleError113.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError113 implements RuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError113 implements RuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError115.php b/src/Rules/RuleErrors/RuleError115.php index 7b5f1af9e5..f6d020a9e2 100644 --- a/src/Rules/RuleErrors/RuleError115.php +++ b/src/Rules/RuleErrors/RuleError115.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError115 implements RuleError, LineRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError115 implements RuleError, LineRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError117.php b/src/Rules/RuleErrors/RuleError117.php index 2492802799..80b8bd5fb2 100644 --- a/src/Rules/RuleErrors/RuleError117.php +++ b/src/Rules/RuleErrors/RuleError117.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError117 implements RuleError, FileRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError117 implements RuleError, FileRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError119.php b/src/Rules/RuleErrors/RuleError119.php index 6d6fb6b7a9..ddee015752 100644 --- a/src/Rules/RuleErrors/RuleError119.php +++ b/src/Rules/RuleErrors/RuleError119.php @@ -12,7 +12,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError119 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError119 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError121.php b/src/Rules/RuleErrors/RuleError121.php index 2c8995a0b3..3a05d8d3c3 100644 --- a/src/Rules/RuleErrors/RuleError121.php +++ b/src/Rules/RuleErrors/RuleError121.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError121 implements RuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError121 implements RuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError123.php b/src/Rules/RuleErrors/RuleError123.php index fedd8de5d9..4bae22b6a5 100644 --- a/src/Rules/RuleErrors/RuleError123.php +++ b/src/Rules/RuleErrors/RuleError123.php @@ -12,7 +12,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError123 implements RuleError, LineRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError123 implements RuleError, LineRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError125.php b/src/Rules/RuleErrors/RuleError125.php index d64be7de95..a24ea70b44 100644 --- a/src/Rules/RuleErrors/RuleError125.php +++ b/src/Rules/RuleErrors/RuleError125.php @@ -12,7 +12,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError125 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError125 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError127.php b/src/Rules/RuleErrors/RuleError127.php index dfb94ecc15..0c2dea58a7 100644 --- a/src/Rules/RuleErrors/RuleError127.php +++ b/src/Rules/RuleErrors/RuleError127.php @@ -13,7 +13,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError127 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError127 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; 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 3ecb8d6233..a606a29b87 100644 --- a/src/Rules/RuleErrors/RuleError13.php +++ b/src/Rules/RuleErrors/RuleError13.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError13 implements RuleError, FileRuleError, TipRuleError +final class RuleError13 implements RuleError, FileRuleError, TipRuleError { public string $message; 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 956f7fe0f0..b952f9a3c5 100644 --- a/src/Rules/RuleErrors/RuleError15.php +++ b/src/Rules/RuleErrors/RuleError15.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError15 implements RuleError, LineRuleError, FileRuleError, TipRuleError +final class RuleError15 implements RuleError, LineRuleError, FileRuleError, TipRuleError { public string $message; 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 827f6a8724..8cdf151a33 100644 --- a/src/Rules/RuleErrors/RuleError17.php +++ b/src/Rules/RuleErrors/RuleError17.php @@ -8,7 +8,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError17 implements RuleError, IdentifierRuleError +final class RuleError17 implements RuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/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 3732da9802..d7a3a4388b 100644 --- a/src/Rules/RuleErrors/RuleError19.php +++ b/src/Rules/RuleErrors/RuleError19.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError19 implements RuleError, LineRuleError, IdentifierRuleError +final class RuleError19 implements RuleError, LineRuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/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 3a6f7eb2d3..91516979e0 100644 --- a/src/Rules/RuleErrors/RuleError21.php +++ b/src/Rules/RuleErrors/RuleError21.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError21 implements RuleError, FileRuleError, IdentifierRuleError +final class RuleError21 implements RuleError, FileRuleError, IdentifierRuleError { public string $message; 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 911a7a05fe..4dcb3e0bae 100644 --- a/src/Rules/RuleErrors/RuleError23.php +++ b/src/Rules/RuleErrors/RuleError23.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError23 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError +final class RuleError23 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError { public string $message; 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 1c5c6001e8..429a1ed0ef 100644 --- a/src/Rules/RuleErrors/RuleError25.php +++ b/src/Rules/RuleErrors/RuleError25.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError25 implements RuleError, TipRuleError, IdentifierRuleError +final class RuleError25 implements RuleError, TipRuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/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 e592c0d98e..6910c787fd 100644 --- a/src/Rules/RuleErrors/RuleError27.php +++ b/src/Rules/RuleErrors/RuleError27.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError27 implements RuleError, LineRuleError, TipRuleError, IdentifierRuleError +final class RuleError27 implements RuleError, LineRuleError, TipRuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError29.php b/src/Rules/RuleErrors/RuleError29.php index 68f71ae5c7..85a6c85960 100644 --- a/src/Rules/RuleErrors/RuleError29.php +++ b/src/Rules/RuleErrors/RuleError29.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError29 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError +final class RuleError29 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError3.php b/src/Rules/RuleErrors/RuleError3.php index ce5c8fffcc..17ab507d2a 100644 --- a/src/Rules/RuleErrors/RuleError3.php +++ b/src/Rules/RuleErrors/RuleError3.php @@ -8,7 +8,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError3 implements RuleError, LineRuleError +final class RuleError3 implements RuleError, LineRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError31.php b/src/Rules/RuleErrors/RuleError31.php index 6402df1887..d9e7665e9b 100644 --- a/src/Rules/RuleErrors/RuleError31.php +++ b/src/Rules/RuleErrors/RuleError31.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError31 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError +final class RuleError31 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError33.php b/src/Rules/RuleErrors/RuleError33.php index 0f37cede7c..692da9a71a 100644 --- a/src/Rules/RuleErrors/RuleError33.php +++ b/src/Rules/RuleErrors/RuleError33.php @@ -8,7 +8,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError33 implements RuleError, MetadataRuleError +final class RuleError33 implements RuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError35.php b/src/Rules/RuleErrors/RuleError35.php index 65868071a8..91c52036bb 100644 --- a/src/Rules/RuleErrors/RuleError35.php +++ b/src/Rules/RuleErrors/RuleError35.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError35 implements RuleError, LineRuleError, MetadataRuleError +final class RuleError35 implements RuleError, LineRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError37.php b/src/Rules/RuleErrors/RuleError37.php index fe0d25a3f5..a92ded0e4f 100644 --- a/src/Rules/RuleErrors/RuleError37.php +++ b/src/Rules/RuleErrors/RuleError37.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError37 implements RuleError, FileRuleError, MetadataRuleError +final class RuleError37 implements RuleError, FileRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError39.php b/src/Rules/RuleErrors/RuleError39.php index 1a50b299fc..7b74800753 100644 --- a/src/Rules/RuleErrors/RuleError39.php +++ b/src/Rules/RuleErrors/RuleError39.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError39 implements RuleError, LineRuleError, FileRuleError, MetadataRuleError +final class RuleError39 implements RuleError, LineRuleError, FileRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError41.php b/src/Rules/RuleErrors/RuleError41.php index 528a20c731..7cc55fdeb1 100644 --- a/src/Rules/RuleErrors/RuleError41.php +++ b/src/Rules/RuleErrors/RuleError41.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError41 implements RuleError, TipRuleError, MetadataRuleError +final class RuleError41 implements RuleError, TipRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError43.php b/src/Rules/RuleErrors/RuleError43.php index 9992c86c9d..a251840dd9 100644 --- a/src/Rules/RuleErrors/RuleError43.php +++ b/src/Rules/RuleErrors/RuleError43.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError43 implements RuleError, LineRuleError, TipRuleError, MetadataRuleError +final class RuleError43 implements RuleError, LineRuleError, TipRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError45.php b/src/Rules/RuleErrors/RuleError45.php index d77f882c50..aceabd9f78 100644 --- a/src/Rules/RuleErrors/RuleError45.php +++ b/src/Rules/RuleErrors/RuleError45.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError45 implements RuleError, FileRuleError, TipRuleError, MetadataRuleError +final class RuleError45 implements RuleError, FileRuleError, TipRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError47.php b/src/Rules/RuleErrors/RuleError47.php index 3a1158c176..866bbc3ddf 100644 --- a/src/Rules/RuleErrors/RuleError47.php +++ b/src/Rules/RuleErrors/RuleError47.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError47 implements RuleError, LineRuleError, FileRuleError, TipRuleError, MetadataRuleError +final class RuleError47 implements RuleError, LineRuleError, FileRuleError, TipRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError49.php b/src/Rules/RuleErrors/RuleError49.php index 6b8aa73a5b..81b0015029 100644 --- a/src/Rules/RuleErrors/RuleError49.php +++ b/src/Rules/RuleErrors/RuleError49.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError49 implements RuleError, IdentifierRuleError, MetadataRuleError +final class RuleError49 implements RuleError, IdentifierRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError5.php b/src/Rules/RuleErrors/RuleError5.php index f8205fd24f..0dbad8299b 100644 --- a/src/Rules/RuleErrors/RuleError5.php +++ b/src/Rules/RuleErrors/RuleError5.php @@ -8,7 +8,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError5 implements RuleError, FileRuleError +final class RuleError5 implements RuleError, FileRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError51.php b/src/Rules/RuleErrors/RuleError51.php index a008143cd2..96d93510c9 100644 --- a/src/Rules/RuleErrors/RuleError51.php +++ b/src/Rules/RuleErrors/RuleError51.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError51 implements RuleError, LineRuleError, IdentifierRuleError, MetadataRuleError +final class RuleError51 implements RuleError, LineRuleError, IdentifierRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError53.php b/src/Rules/RuleErrors/RuleError53.php index cd8418f5b2..1e11f5e641 100644 --- a/src/Rules/RuleErrors/RuleError53.php +++ b/src/Rules/RuleErrors/RuleError53.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError53 implements RuleError, FileRuleError, IdentifierRuleError, MetadataRuleError +final class RuleError53 implements RuleError, FileRuleError, IdentifierRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError55.php b/src/Rules/RuleErrors/RuleError55.php index 4eb281839d..3bf4a22ccf 100644 --- a/src/Rules/RuleErrors/RuleError55.php +++ b/src/Rules/RuleErrors/RuleError55.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError55 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, MetadataRuleError +final class RuleError55 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError57.php b/src/Rules/RuleErrors/RuleError57.php index 4fabd64eac..22c77fc545 100644 --- a/src/Rules/RuleErrors/RuleError57.php +++ b/src/Rules/RuleErrors/RuleError57.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError57 implements RuleError, TipRuleError, IdentifierRuleError, MetadataRuleError +final class RuleError57 implements RuleError, TipRuleError, IdentifierRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError59.php b/src/Rules/RuleErrors/RuleError59.php index 837c62602d..a7659febe1 100644 --- a/src/Rules/RuleErrors/RuleError59.php +++ b/src/Rules/RuleErrors/RuleError59.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError59 implements RuleError, LineRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError +final class RuleError59 implements RuleError, LineRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError61.php b/src/Rules/RuleErrors/RuleError61.php index a861ab2f51..723a0aa79b 100644 --- a/src/Rules/RuleErrors/RuleError61.php +++ b/src/Rules/RuleErrors/RuleError61.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError61 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError +final class RuleError61 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError63.php b/src/Rules/RuleErrors/RuleError63.php index 919587a216..1c88f9fbc2 100644 --- a/src/Rules/RuleErrors/RuleError63.php +++ b/src/Rules/RuleErrors/RuleError63.php @@ -12,7 +12,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError63 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError +final class RuleError63 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError65.php b/src/Rules/RuleErrors/RuleError65.php index 095f49475d..fc2593bbfa 100644 --- a/src/Rules/RuleErrors/RuleError65.php +++ b/src/Rules/RuleErrors/RuleError65.php @@ -8,7 +8,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError65 implements RuleError, NonIgnorableRuleError +final class RuleError65 implements RuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError67.php b/src/Rules/RuleErrors/RuleError67.php index 08a214f997..b2218268c3 100644 --- a/src/Rules/RuleErrors/RuleError67.php +++ b/src/Rules/RuleErrors/RuleError67.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError67 implements RuleError, LineRuleError, NonIgnorableRuleError +final class RuleError67 implements RuleError, LineRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError69.php b/src/Rules/RuleErrors/RuleError69.php index 75cd512c3e..7f5e130f09 100644 --- a/src/Rules/RuleErrors/RuleError69.php +++ b/src/Rules/RuleErrors/RuleError69.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError69 implements RuleError, FileRuleError, NonIgnorableRuleError +final class RuleError69 implements RuleError, FileRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError7.php b/src/Rules/RuleErrors/RuleError7.php index af9559cfaa..203696b2fd 100644 --- a/src/Rules/RuleErrors/RuleError7.php +++ b/src/Rules/RuleErrors/RuleError7.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError7 implements RuleError, LineRuleError, FileRuleError +final class RuleError7 implements RuleError, LineRuleError, FileRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError71.php b/src/Rules/RuleErrors/RuleError71.php index 652b0f1922..d78d2813e3 100644 --- a/src/Rules/RuleErrors/RuleError71.php +++ b/src/Rules/RuleErrors/RuleError71.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError71 implements RuleError, LineRuleError, FileRuleError, NonIgnorableRuleError +final class RuleError71 implements RuleError, LineRuleError, FileRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError73.php b/src/Rules/RuleErrors/RuleError73.php index 8cdaeaa36d..fd81121a2d 100644 --- a/src/Rules/RuleErrors/RuleError73.php +++ b/src/Rules/RuleErrors/RuleError73.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError73 implements RuleError, TipRuleError, NonIgnorableRuleError +final class RuleError73 implements RuleError, TipRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError75.php b/src/Rules/RuleErrors/RuleError75.php index 3195db7454..d249eef75e 100644 --- a/src/Rules/RuleErrors/RuleError75.php +++ b/src/Rules/RuleErrors/RuleError75.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError75 implements RuleError, LineRuleError, TipRuleError, NonIgnorableRuleError +final class RuleError75 implements RuleError, LineRuleError, TipRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError77.php b/src/Rules/RuleErrors/RuleError77.php index 09edd26a3d..e0e8547a32 100644 --- a/src/Rules/RuleErrors/RuleError77.php +++ b/src/Rules/RuleErrors/RuleError77.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError77 implements RuleError, FileRuleError, TipRuleError, NonIgnorableRuleError +final class RuleError77 implements RuleError, FileRuleError, TipRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError79.php b/src/Rules/RuleErrors/RuleError79.php index 3c1fcf4d23..3a07eea396 100644 --- a/src/Rules/RuleErrors/RuleError79.php +++ b/src/Rules/RuleErrors/RuleError79.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError79 implements RuleError, LineRuleError, FileRuleError, TipRuleError, NonIgnorableRuleError +final class RuleError79 implements RuleError, LineRuleError, FileRuleError, TipRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError81.php b/src/Rules/RuleErrors/RuleError81.php index 1412335a8a..fe96c09839 100644 --- a/src/Rules/RuleErrors/RuleError81.php +++ b/src/Rules/RuleErrors/RuleError81.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError81 implements RuleError, IdentifierRuleError, NonIgnorableRuleError +final class RuleError81 implements RuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError83.php b/src/Rules/RuleErrors/RuleError83.php index ceb2141456..9570715ebe 100644 --- a/src/Rules/RuleErrors/RuleError83.php +++ b/src/Rules/RuleErrors/RuleError83.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError83 implements RuleError, LineRuleError, IdentifierRuleError, NonIgnorableRuleError +final class RuleError83 implements RuleError, LineRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError85.php b/src/Rules/RuleErrors/RuleError85.php index a0d13d45de..5af535902a 100644 --- a/src/Rules/RuleErrors/RuleError85.php +++ b/src/Rules/RuleErrors/RuleError85.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError85 implements RuleError, FileRuleError, IdentifierRuleError, NonIgnorableRuleError +final class RuleError85 implements RuleError, FileRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError87.php b/src/Rules/RuleErrors/RuleError87.php index 386b844a1b..44028fe427 100644 --- a/src/Rules/RuleErrors/RuleError87.php +++ b/src/Rules/RuleErrors/RuleError87.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError87 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, NonIgnorableRuleError +final class RuleError87 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError89.php b/src/Rules/RuleErrors/RuleError89.php index 3b207cf2ad..e69b4058a6 100644 --- a/src/Rules/RuleErrors/RuleError89.php +++ b/src/Rules/RuleErrors/RuleError89.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError89 implements RuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError +final class RuleError89 implements RuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError9.php b/src/Rules/RuleErrors/RuleError9.php index e93934d20b..c8454faf62 100644 --- a/src/Rules/RuleErrors/RuleError9.php +++ b/src/Rules/RuleErrors/RuleError9.php @@ -8,7 +8,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError9 implements RuleError, TipRuleError +final class RuleError9 implements RuleError, TipRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError91.php b/src/Rules/RuleErrors/RuleError91.php index a0fc78df5a..8c11c1816f 100644 --- a/src/Rules/RuleErrors/RuleError91.php +++ b/src/Rules/RuleErrors/RuleError91.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError91 implements RuleError, LineRuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError +final class RuleError91 implements RuleError, LineRuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError93.php b/src/Rules/RuleErrors/RuleError93.php index 88b7282eb2..8c5b9c5a64 100644 --- a/src/Rules/RuleErrors/RuleError93.php +++ b/src/Rules/RuleErrors/RuleError93.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError93 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError +final class RuleError93 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError95.php b/src/Rules/RuleErrors/RuleError95.php index 0fbb2a635b..68b993db00 100644 --- a/src/Rules/RuleErrors/RuleError95.php +++ b/src/Rules/RuleErrors/RuleError95.php @@ -12,7 +12,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError95 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError +final class RuleError95 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError97.php b/src/Rules/RuleErrors/RuleError97.php index da07902233..da13e04d88 100644 --- a/src/Rules/RuleErrors/RuleError97.php +++ b/src/Rules/RuleErrors/RuleError97.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError97 implements RuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError97 implements RuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError99.php b/src/Rules/RuleErrors/RuleError99.php index b26457a1e1..60c3af565f 100644 --- a/src/Rules/RuleErrors/RuleError99.php +++ b/src/Rules/RuleErrors/RuleError99.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError99 implements RuleError, LineRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError99 implements RuleError, LineRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index c5a08ba5ab..109b30a2b3 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -4,6 +4,8 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\CallableType; @@ -14,32 +16,35 @@ 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 PHPStan\Type\VerbosityLevel; -use function array_merge; use function count; use function sprintf; -use function str_contains; -class RuleLevelHelper +#[AutowiredService] +final class RuleLevelHelper { public function __construct( private ReflectionProvider $reflectionProvider, + #[AutowiredParameter] private bool $checkNullables, + #[AutowiredParameter] private bool $checkThisOnly, + #[AutowiredParameter] private bool $checkUnionTypes, + #[AutowiredParameter] private bool $checkExplicitMixed, + #[AutowiredParameter] private bool $checkImplicitMixed, - private bool $newRuleLevelHelper, + #[AutowiredParameter] private bool $checkBenevolentUnionTypes, + #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] + private bool $discoveringSymbolsTip, ) { } @@ -50,12 +55,6 @@ 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 - { - return $this->acceptsWithReason($acceptingType, $acceptedType, $strictTypes)->result; - } - private function transformCommonType(Type $type): Type { if (!$this->checkExplicitMixed && !$this->checkImplicitMixed) { @@ -64,10 +63,6 @@ private function transformCommonType(Type $type): Type return TypeTraverser::map($type, function (Type $type, callable $traverse) { if ($type instanceof TemplateMixedType) { - if (!$this->newRuleLevelHelper) { - return $type->toStrictMixedType(); - } - if ($this->checkExplicitMixed) { return $type->toStrictMixedType(); } @@ -124,6 +119,9 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType): $acceptedType->getTemplateTags(), $acceptedType->getThrowPoints(), $acceptedType->getImpurePoints(), + $acceptedType->getInvalidateExpressions(), + $acceptedType->getUsedVariables(), + $acceptedType->acceptsNamedArguments(), ); } @@ -149,150 +147,13 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType): return [$acceptedType, $checkForUnion]; } - public function acceptsWithReason(Type $acceptingType, Type $acceptedType, bool $strictTypes): RuleLevelHelperAcceptsResult + /** @api */ + public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTypes): RuleLevelHelperAcceptsResult { - if ($this->newRuleLevelHelper) { - [$acceptedType, $checkForUnion] = $this->transformAcceptedType($acceptingType, $acceptedType); - $acceptingType = $this->transformCommonType($acceptingType); - - $accepts = $acceptingType->acceptsWithReason($acceptedType, $strictTypes); - - return new RuleLevelHelperAcceptsResult( - $checkForUnion ? $accepts->yes() : !$accepts->no(), - $accepts->reasons, - ); - } - - $checkForUnion = $this->checkUnionTypes; - - if ($this->checkBenevolentUnionTypes) { - $traverse = static function (Type $type, callable $traverse) use (&$checkForUnion): Type { - if ($type instanceof BenevolentUnionType) { - $checkForUnion = true; - return TypeUtils::toStrictUnion($type); - } - - return $traverse($type); - }; - - $acceptedType = TypeTraverser::map($acceptedType, $traverse); - } - - if ( - $this->checkExplicitMixed - ) { - $traverse = static function (Type $type, callable $traverse): Type { - if ($type instanceof TemplateMixedType) { - return $type->toStrictMixedType(); - } - if ( - $type instanceof MixedType - && $type->isExplicitMixed() - ) { - return new StrictMixedType(); - } - - return $traverse($type); - }; - $acceptingType = TypeTraverser::map($acceptingType, $traverse); - $acceptedType = TypeTraverser::map($acceptedType, $traverse); - } + [$acceptedType, $checkForUnion] = $this->transformAcceptedType($acceptingType, $acceptedType); + $acceptingType = $this->transformCommonType($acceptingType); - if ( - $this->checkImplicitMixed - ) { - $traverse = static function (Type $type, callable $traverse): Type { - if ($type instanceof TemplateMixedType) { - return $type->toStrictMixedType(); - } - if ( - $type instanceof MixedType - && !$type->isExplicitMixed() - ) { - return new StrictMixedType(); - } - - return $traverse($type); - }; - $acceptingType = TypeTraverser::map($acceptingType, $traverse); - $acceptedType = TypeTraverser::map($acceptedType, $traverse); - } - - if ( - !$this->checkNullables - && !$acceptingType instanceof NullType - && !$acceptedType instanceof NullType - && !$acceptedType instanceof BenevolentUnionType - ) { - $acceptedType = TypeCombinator::removeNull($acceptedType); - } - - $accepts = $acceptingType->acceptsWithReason($acceptedType, $strictTypes); - if ($accepts->yes()) { - return new RuleLevelHelperAcceptsResult(true, $accepts->reasons); - } - if ($acceptingType instanceof UnionType) { - $reasons = []; - foreach ($acceptingType->getTypes() as $innerType) { - $accepts = self::acceptsWithReason($innerType, $acceptedType, $strictTypes); - if ($accepts->result) { - return $accepts; - } - - $reasons = array_merge($reasons, $accepts->reasons); - } - - return new RuleLevelHelperAcceptsResult(false, $reasons); - } - - if ( - $acceptedType->isArray()->yes() - && $acceptingType->isArray()->yes() - && ( - $acceptedType->isConstantArray()->no() - || !$acceptedType->isIterableAtLeastOnce()->no() - ) - && $acceptingType->isConstantArray()->no() - ) { - if ($acceptingType->isIterableAtLeastOnce()->yes() && !$acceptedType->isIterableAtLeastOnce()->yes()) { - $verbosity = VerbosityLevel::getRecommendedLevelByType($acceptingType, $acceptedType); - return new RuleLevelHelperAcceptsResult(false, [ - sprintf( - '%s %s empty.', - $acceptedType->describe($verbosity), - $acceptedType->isIterableAtLeastOnce()->no() ? 'is' : 'might be', - ), - ]); - } - - if ( - $acceptingType->isList()->yes() - && !$acceptedType->isList()->yes() - ) { - $report = $checkForUnion || $acceptedType->isList()->no(); - - if ($report) { - $verbosity = VerbosityLevel::getRecommendedLevelByType($acceptingType, $acceptedType); - return new RuleLevelHelperAcceptsResult(false, [ - sprintf( - '%s %s a list.', - $acceptedType->describe($verbosity), - $acceptedType->isList()->no() ? 'is not' : 'might not be', - ), - ]); - } - } - - return self::acceptsWithReason( - $acceptingType->getIterableKeyType(), - $acceptedType->getIterableKeyType(), - $strictTypes, - )->and(self::acceptsWithReason( - $acceptingType->getIterableValueType(), - $acceptedType->getIterableValueType(), - $strictTypes, - )); - } + $accepts = $acceptingType->accepts($acceptedType, $strictTypes); return new RuleLevelHelperAcceptsResult( $checkForUnion ? $accepts->yes() : !$accepts->no(), @@ -333,49 +194,24 @@ private function findTypeToCheckImplementation( $type = TypeCombinator::removeNull($type); } - if ($this->newRuleLevelHelper) { - if ( - ($this->checkExplicitMixed || $this->checkImplicitMixed) - && $type instanceof MixedType - && ($type->isExplicitMixed() ? $this->checkExplicitMixed : $this->checkImplicitMixed) - ) { - return new FoundTypeResult( - $type instanceof TemplateMixedType - ? $type->toStrictMixedType() - : new StrictMixedType(), - [], - [], - null, - ); - } - } else { - if ( - $this->checkExplicitMixed - && $type instanceof MixedType - && !$type instanceof TemplateMixedType - && $type->isExplicitMixed() - ) { - return new FoundTypeResult(new StrictMixedType(), [], [], null); - } - - if ( - $this->checkImplicitMixed - && $type instanceof MixedType - && !$type instanceof TemplateMixedType - && !$type->isExplicitMixed() - ) { - return new FoundTypeResult(new StrictMixedType(), [], [], null); - } + if ( + ($this->checkExplicitMixed || $this->checkImplicitMixed) + && $type instanceof MixedType + && ($type->isExplicitMixed() ? $this->checkExplicitMixed : $this->checkImplicitMixed) + ) { + return new FoundTypeResult( + $type instanceof TemplateMixedType + ? $type->toStrictMixedType() + : new StrictMixedType(), + [], + [], + null, + ); } if ($type instanceof MixedType || $type instanceof NeverType) { return new FoundTypeResult(new ErrorType(), [], [], null); } - if (!$this->newRuleLevelHelper) { - if ($isTopLevel && $type instanceof StaticType) { - $type = $type->getStaticObjectType(); - } - } $errors = []; $hasClassExistsClass = false; @@ -396,11 +232,15 @@ private function findTypeToCheckImplementation( continue; } - $errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass)) + $errorBuilder = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass)) ->line($var->getStartLine()) - ->identifier('class.notFound') - ->discoveringSymbolsTip() - ->build(); + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); } } @@ -408,93 +248,81 @@ private function findTypeToCheckImplementation( return new FoundTypeResult(new ErrorType(), [], $errors, null); } - if (!$this->checkUnionTypes && $type instanceof ObjectWithoutClassType) { + if (!$this->checkUnionTypes && $type->isObject()->yes() && count($type->getObjectClassNames()) === 0) { return new FoundTypeResult(new ErrorType(), [], [], null); } - if ($this->newRuleLevelHelper) { - if ($type instanceof UnionType) { - $shouldFilterUnion = ( - !$this->checkUnionTypes - && !$type instanceof BenevolentUnionType - ) || ( - !$this->checkBenevolentUnionTypes - && $type instanceof BenevolentUnionType - ); - - $newTypes = []; + if ($type instanceof UnionType) { + $shouldFilterUnion = ( + !$this->checkUnionTypes + && !$type instanceof BenevolentUnionType + ) || ( + !$this->checkBenevolentUnionTypes + && $type instanceof BenevolentUnionType + ); - foreach ($type->getTypes() as $innerType) { - if ($shouldFilterUnion && !$unionTypeCriteriaCallback($innerType)) { - continue; - } + $newTypes = []; - $newTypes[] = $this->findTypeToCheckImplementation( - $scope, - $var, - $innerType, - $unknownClassErrorPattern, - $unionTypeCriteriaCallback, - )->getType(); + foreach ($type->getTypes() as $innerType) { + if ($shouldFilterUnion && !$unionTypeCriteriaCallback($innerType)) { + continue; } - if (count($newTypes) > 0) { - $newUnion = TypeCombinator::union(...$newTypes); - if ( - !$this->checkBenevolentUnionTypes - && $type instanceof BenevolentUnionType - ) { - $newUnion = TypeUtils::toBenevolentUnion($newUnion); - } + $newTypes[] = $this->findTypeToCheckImplementation( + $scope, + $var, + $innerType, + $unknownClassErrorPattern, + $unionTypeCriteriaCallback, + )->getType(); + } - return new FoundTypeResult($newUnion, $directClassNames, [], null); + if (count($newTypes) > 0) { + $newUnion = TypeCombinator::union(...$newTypes); + if ( + !$this->checkBenevolentUnionTypes + && $type instanceof BenevolentUnionType + ) { + $newUnion = TypeUtils::toBenevolentUnion($newUnion); } + + return new FoundTypeResult($newUnion, $directClassNames, [], null); } + } - if ($type instanceof IntersectionType) { - $newTypes = []; + if ($type instanceof IntersectionType) { + $newTypes = []; - foreach ($type->getTypes() as $innerType) { + $changed = false; + foreach ($type->getTypes() as $innerType) { + if ($innerType instanceof TemplateMixedType) { + $changed = true; $newTypes[] = $this->findTypeToCheckImplementation( $scope, $var, - $innerType, + $innerType->toStrictMixedType(), $unknownClassErrorPattern, $unionTypeCriteriaCallback, )->getType(); + continue; } - - return new FoundTypeResult(TypeCombinator::intersect(...$newTypes), $directClassNames, [], null); + $newTypes[] = $innerType; } - } else { - if ( - ( - !$this->checkUnionTypes - && $type instanceof UnionType - && !$type instanceof BenevolentUnionType - ) || ( - !$this->checkBenevolentUnionTypes - && $type instanceof BenevolentUnionType - ) - ) { - $newTypes = []; - - foreach ($type->getTypes() as $innerType) { - if (!$unionTypeCriteriaCallback($innerType)) { - continue; - } - - $newTypes[] = $innerType; - } - if (count($newTypes) > 0) { - return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null); - } + if ($changed) { + return new FoundTypeResult(TypeCombinator::intersect(...$newTypes), $directClassNames, [], null); } } $tip = null; - if (str_contains($type->describe(VerbosityLevel::typeOnly()), 'PhpParser\\Node\\Arg|PhpParser\\Node\\VariadicPlaceholder') && !$unionTypeCriteriaCallback($type)) { + 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.'; } diff --git a/src/Rules/RuleLevelHelperAcceptsResult.php b/src/Rules/RuleLevelHelperAcceptsResult.php index 201408f8f5..1b421c60a4 100644 --- a/src/Rules/RuleLevelHelperAcceptsResult.php +++ b/src/Rules/RuleLevelHelperAcceptsResult.php @@ -4,7 +4,10 @@ use function array_merge; -class RuleLevelHelperAcceptsResult +/** + * @api + */ +final class RuleLevelHelperAcceptsResult { /** diff --git a/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php index 2b5c809341..de3111883d 100644 --- a/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InArrowFunctionNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,7 +15,8 @@ /** * @implements Rule */ -class TooWideArrowFunctionReturnTypehintRule implements Rule +#[RegisteredRule(level: 4)] +final class TooWideArrowFunctionReturnTypehintRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php index 77879f99a3..76e8800284 100644 --- a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClosureReturnStatementsNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -16,7 +17,8 @@ /** * @implements Rule */ -class TooWideClosureReturnTypehintRule implements Rule +#[RegisteredRule(level: 4)] +final class TooWideClosureReturnTypehintRule implements Rule { public function getNodeType(): string diff --git a/src/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRule.php b/src/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRule.php index 8056f39420..c7bfc524cc 100644 --- a/src/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRule.php +++ b/src/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRule.php @@ -4,15 +4,16 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\FunctionReturnStatementsNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use function sprintf; /** * @implements Rule */ -class TooWideFunctionParameterOutTypeRule implements Rule +#[RegisteredRule(level: 4)] +final class TooWideFunctionParameterOutTypeRule implements Rule { public function __construct( @@ -33,7 +34,7 @@ public function processNode(Node $node, Scope $scope): array return $this->check->check( $node->getExecutionEnds(), $node->getReturnStatements(), - ParametersAcceptorSelector::selectSingle($inFunction->getVariants())->getParameters(), + $inFunction->getParameters(), sprintf('Function %s()', $inFunction->getName()), ); } diff --git a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php index 9451787f4d..9a83747ce5 100644 --- a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php @@ -4,21 +4,23 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\FunctionReturnStatementsNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use PHPStan\Type\VoidType; use function count; use function sprintf; /** * @implements Rule */ -class TooWideFunctionReturnTypehintRule implements Rule +#[RegisteredRule(level: 4)] +final class TooWideFunctionReturnTypehintRule implements Rule { public function getNodeType(): string @@ -30,7 +32,7 @@ public function processNode(Node $node, Scope $scope): array { $function = $node->getFunctionReflection(); - $functionReturnType = ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(); + $functionReturnType = $function->getReturnType(); $functionReturnType = TypeUtils::resolveLateResolvableTypes($functionReturnType); if (!$functionReturnType instanceof UnionType) { return []; @@ -49,14 +51,15 @@ public function processNode(Node $node, Scope $scope): array foreach ($returnStatements as $returnStatement) { $returnNode = $returnStatement->getReturnNode(); if ($returnNode->expr === null) { + $returnTypes[] = new VoidType(); continue; } $returnTypes[] = $returnStatement->getScope()->getType($returnNode->expr); } - if (count($returnTypes) === 0) { - return []; + if (!$statementResult->isAlwaysTerminating()) { + $returnTypes[] = new VoidType(); } $returnType = TypeCombinator::union(...$returnTypes); diff --git a/src/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRule.php b/src/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRule.php index e715ce7d91..6b580f0888 100644 --- a/src/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRule.php +++ b/src/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRule.php @@ -4,15 +4,16 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\MethodReturnStatementsNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use function sprintf; /** * @implements Rule */ -class TooWideMethodParameterOutTypeRule implements Rule +#[RegisteredRule(level: 4)] +final class TooWideMethodParameterOutTypeRule implements Rule { public function __construct( @@ -33,7 +34,7 @@ public function processNode(Node $node, Scope $scope): array return $this->check->check( $node->getExecutionEnds(), $node->getReturnStatements(), - ParametersAcceptorSelector::selectSingle($inMethod->getVariants())->getParameters(), + $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 5e62501f72..750dd7375a 100644 --- a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php @@ -4,25 +4,30 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\MethodReturnStatementsNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use PHPStan\Type\VoidType; use function count; use function sprintf; /** * @implements Rule */ -class TooWideMethodReturnTypehintRule implements Rule +#[RegisteredRule(level: 4)] +final class TooWideMethodReturnTypehintRule implements Rule { - public function __construct(private bool $checkProtectedAndPublicMethods, private bool $alwaysCheckFinal) + public function __construct( + #[AutowiredParameter(ref: '%checkTooWideReturnTypesInProtectedAndPublicMethods%')] + private bool $checkProtectedAndPublicMethods, + ) { } @@ -39,24 +44,18 @@ public function processNode(Node $node, Scope $scope): array $method = $node->getMethodReflection(); $isFirstDeclaration = $method->getPrototype()->getDeclaringClass() === $method->getDeclaringClass(); if (!$method->isPrivate()) { - if ($this->alwaysCheckFinal) { - if (!$method->getDeclaringClass()->isFinal() && !$method->isFinal()->yes()) { - if (!$this->checkProtectedAndPublicMethods) { - return []; - } + if (!$method->getDeclaringClass()->isFinal() && !$method->isFinal()->yes()) { + if (!$this->checkProtectedAndPublicMethods) { + return []; + } - if ($isFirstDeclaration) { - return []; - } + if ($isFirstDeclaration) { + return []; } - } elseif (!$this->checkProtectedAndPublicMethods) { - return []; - } elseif ($isFirstDeclaration && !$method->getDeclaringClass()->isFinal() && !$method->isFinal()->yes()) { - return []; } } - $methodReturnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType(); + $methodReturnType = $method->getReturnType(); $methodReturnType = TypeUtils::resolveLateResolvableTypes($methodReturnType); if (!$methodReturnType instanceof UnionType) { return []; @@ -75,21 +74,22 @@ public function processNode(Node $node, Scope $scope): array foreach ($returnStatements as $returnStatement) { $returnNode = $returnStatement->getReturnNode(); if ($returnNode->expr === null) { + $returnTypes[] = new VoidType(); continue; } $returnTypes[] = $returnStatement->getScope()->getType($returnNode->expr); } - if (count($returnTypes) === 0) { - return []; + if (!$statementResult->isAlwaysTerminating()) { + $returnTypes[] = new VoidType(); } $returnType = TypeCombinator::union(...$returnTypes); if ( - !$method->isPrivate() - && ($returnType->isNull()->yes() || $returnType instanceof ConstantBooleanType) - && !$isFirstDeclaration + !$isFirstDeclaration + && !$method->isPrivate() + && ($returnType->isNull()->yes() || $returnType->isTrue()->yes() || $returnType->isFalse()->yes()) ) { return []; } diff --git a/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php b/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php index 097812f918..47ff428789 100644 --- a/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php +++ b/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php @@ -4,9 +4,10 @@ use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\ExecutionEndNode; use PHPStan\Node\ReturnStatement; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; +use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\TypeUtils; @@ -14,13 +15,14 @@ use PHPStan\Type\VerbosityLevel; use function sprintf; -class TooWideParameterOutTypeCheck +#[AutowiredService] +final class TooWideParameterOutTypeCheck { /** * @param list $executionEnds * @param list $returnStatements - * @param ParameterReflectionWithPhpDocs[] $parameters + * @param ExtendedParameterReflection[] $parameters * @return list */ public function check( @@ -74,7 +76,7 @@ public function check( private function processSingleParameter( Scope $scope, string $functionDescription, - ParameterReflectionWithPhpDocs $parameter, + ExtendedParameterReflection $parameter, ): array { $isParamOutType = true; diff --git a/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php new file mode 100644 index 0000000000..f3b03d0dfa --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php @@ -0,0 +1,137 @@ + + */ +#[RegisteredRule(level: 4)] +final class TooWidePropertyTypeRule implements Rule +{ + + public function __construct( + private ReadWritePropertiesExtensionProvider $extensionProvider, + private PropertyReflectionFinder $propertyReflectionFinder, + ) + { + } + + public function getNodeType(): string + { + return ClassPropertiesNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + $classReflection = $node->getClassReflection(); + + foreach ($node->getProperties() as $property) { + if (!$property->isPrivate()) { + continue; + } + if ($property->isDeclaredInTrait()) { + continue; + } + if ($property->isPromoted()) { + continue; + } + $propertyName = $property->getName(); + if (!$classReflection->hasNativeProperty($propertyName)) { + continue; + } + + $propertyReflection = $classReflection->getNativeProperty($propertyName); + $propertyType = $propertyReflection->getWritableType(); + if (!$propertyType instanceof UnionType) { + continue; + } + foreach ($this->extensionProvider->getExtensions() as $extension) { + if ($extension->isAlwaysRead($propertyReflection, $propertyName)) { + continue 2; + } + if ($extension->isAlwaysWritten($propertyReflection, $propertyName)) { + continue 2; + } + if ($extension->isInitialized($propertyReflection, $propertyName)) { + continue 2; + } + } + + $assignedTypes = []; + foreach ($node->getPropertyAssigns() as $assign) { + $assignNode = $assign->getAssign(); + $assignPropertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($assignNode->getPropertyFetch(), $assign->getScope()); + foreach ($assignPropertyReflections as $assignPropertyReflection) { + if ($propertyName !== $assignPropertyReflection->getName()) { + continue; + } + if ($propertyReflection->getDeclaringClass()->getName() !== $assignPropertyReflection->getDeclaringClass()->getName()) { + continue; + } + + $assignedTypes[] = $assignPropertyReflection->getScope()->getType($assignNode->getAssignedExpr()); + } + } + + if ($property->getDefault() !== null) { + $assignedTypes[] = $scope->getType($property->getDefault()); + } + + if (count($assignedTypes) === 0) { + continue; + } + + $assignedType = TypeCombinator::union(...$assignedTypes); + $propertyDescription = $this->describePropertyByName($propertyReflection, $propertyName); + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($propertyType, $assignedType); + foreach ($propertyType->getTypes() as $type) { + if (!$type->isSuperTypeOf($assignedType)->no()) { + continue; + } + + if ($property->getNativeType() === null && (new NullType())->isSuperTypeOf($type)->yes()) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + '%s (%s) is never assigned %s so it can be removed from the property type.', + $propertyDescription, + $propertyType->describe($verbosityLevel), + $type->describe($verbosityLevel), + )) + ->identifier('property.unusedType') + ->line($property->getStartLine()) + ->build(); + } + + } + return $errors; + } + + private function describePropertyByName(PropertyReflection $property, string $propertyName): string + { + if (!$property->isStatic()) { + return sprintf('Property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $propertyName); + } + + return sprintf('Static property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $propertyName); + } + +} diff --git a/src/Rules/Traits/ConflictingTraitConstantsRule.php b/src/Rules/Traits/ConflictingTraitConstantsRule.php index 73612f3074..63b1d047a2 100644 --- a/src/Rules/Traits/ConflictingTraitConstantsRule.php +++ b/src/Rules/Traits/ConflictingTraitConstantsRule.php @@ -5,9 +5,11 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClassConstant; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -19,10 +21,14 @@ /** * @implements Rule */ -class ConflictingTraitConstantsRule implements Rule +#[RegisteredRule(level: 0)] +final class ConflictingTraitConstantsRule implements Rule { - public function __construct(private InitializerExprTypeResolver $initializerExprTypeResolver) + public function __construct( + private InitializerExprTypeResolver $initializerExprTypeResolver, + private ReflectionProvider $reflectionProvider, + ) { } @@ -186,7 +192,7 @@ private function processSingleConstant(ClassReflection $classReflection, Reflect ->build(); } } elseif ($constantNativeType === null) { - $traitNativeTypeType = TypehintHelper::decideTypeFromReflection($traitNativeType, null, $traitDeclaringClass->getName()); + $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(), @@ -200,7 +206,7 @@ private function processSingleConstant(ClassReflection $classReflection, Reflect ->identifier('classConstant.missingNativeType') ->build(); } else { - $traitNativeTypeType = TypehintHelper::decideTypeFromReflection($traitNativeType, null, $traitDeclaringClass->getName()); + $traitNativeTypeType = TypehintHelper::decideTypeFromReflection($traitNativeType, selfClass: $this->reflectionProvider->getClass($traitDeclaringClass->getName())); $constantNativeTypeType = ParserNodeTypeToPHPStanType::resolve($constantNativeType, $classReflection); if (!$traitNativeTypeType->equals($constantNativeTypeType)) { $errors[] = RuleErrorBuilder::message(sprintf( diff --git a/src/Rules/Traits/ConstantsInTraitsRule.php b/src/Rules/Traits/ConstantsInTraitsRule.php index 6948568540..d2d146f67f 100644 --- a/src/Rules/Traits/ConstantsInTraitsRule.php +++ b/src/Rules/Traits/ConstantsInTraitsRule.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,7 +12,8 @@ /** * @implements Rule */ -class ConstantsInTraitsRule implements Rule +#[RegisteredRule(level: 0)] +final class ConstantsInTraitsRule implements Rule { public function __construct(private PhpVersion $phpVersion) diff --git a/src/Rules/Traits/NotAnalysedTraitRule.php b/src/Rules/Traits/NotAnalysedTraitRule.php index 3ae9b74c46..1cb97f91d2 100644 --- a/src/Rules/Traits/NotAnalysedTraitRule.php +++ b/src/Rules/Traits/NotAnalysedTraitRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\CollectedDataNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -13,7 +14,8 @@ /** * @implements Rule */ -class NotAnalysedTraitRule implements Rule +#[RegisteredRule(level: 4)] +final class NotAnalysedTraitRule implements Rule { public function getNodeType(): string 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 index 7ce2cee84d..ee430f7560 100644 --- a/src/Rules/Traits/TraitDeclarationCollector.php +++ b/src/Rules/Traits/TraitDeclarationCollector.php @@ -5,11 +5,13 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Collectors\Collector; +use PHPStan\DependencyInjection\RegisteredCollector; /** * @implements Collector */ -class TraitDeclarationCollector implements Collector +#[RegisteredCollector(level: 4)] +final class TraitDeclarationCollector implements Collector { public function getNodeType(): string diff --git a/src/Rules/Traits/TraitUseCollector.php b/src/Rules/Traits/TraitUseCollector.php index 1bd3f82cba..3d5976f920 100644 --- a/src/Rules/Traits/TraitUseCollector.php +++ b/src/Rules/Traits/TraitUseCollector.php @@ -5,13 +5,15 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Collectors\Collector; +use PHPStan\DependencyInjection\RegisteredCollector; use function array_map; use function array_values; /** * @implements Collector> */ -class TraitUseCollector implements Collector +#[RegisteredCollector(level: 4)] +final class TraitUseCollector implements Collector { public function getNodeType(): string @@ -19,7 +21,10 @@ public function getNodeType(): string return Node\Stmt\TraitUse::class; } - public function processNode(Node $node, Scope $scope) + /** + * @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 index 88442c2bde..cad8a265f9 100644 --- a/src/Rules/Types/InvalidTypesInUnionRule.php +++ b/src/Rules/Types/InvalidTypesInUnionRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClassPropertyNode; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; @@ -15,7 +16,8 @@ /** * @implements Rule */ -class InvalidTypesInUnionRule implements Rule +#[RegisteredRule(level: 0)] +final class InvalidTypesInUnionRule implements Rule { private const ONLY_STANDALONE_TYPES = [ @@ -69,11 +71,11 @@ private function processFunctionLikeNode(Node\FunctionLike $functionLike): array */ private function processClassPropertyNode(ClassPropertyNode $classPropertyNode): array { - if (!$classPropertyNode->getNativeType() instanceof Node\ComplexType) { + if (!$classPropertyNode->getNativeTypeNode() instanceof Node\ComplexType) { return []; } - return $this->processComplexType($classPropertyNode->getNativeType()); + return $this->processComplexType($classPropertyNode->getNativeTypeNode()); } /** diff --git a/src/Rules/UnusedFunctionParametersCheck.php b/src/Rules/UnusedFunctionParametersCheck.php index 8da2427d31..10ec0753e1 100644 --- a/src/Rules/UnusedFunctionParametersCheck.php +++ b/src/Rules/UnusedFunctionParametersCheck.php @@ -3,38 +3,53 @@ 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 function array_fill_keys; -use function array_keys; +use PHPStan\ShouldNotHappenException; +use function array_combine; +use function array_map; 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 { - public function __construct(private ReflectionProvider $reflectionProvider) + public function __construct( + private ReflectionProvider $reflectionProvider, + #[AutowiredParameter(ref: '%featureToggles.reportPreciseLineForUnusedFunctionParameter%')] + private bool $reportExactLine, + ) { } /** - * @param string[] $parameterNames + * @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 { - $unusedParameters = array_fill_keys($parameterNames, true); + $parameterNames = array_map(static function (Variable $variable): string { + if (!is_string($variable->name)) { + throw new ShouldNotHappenException(); + } + return $variable->name; + }, $parameterVars); + $unusedParameters = array_combine($parameterNames, $parameterVars); foreach ($this->getUsedVariables($scope, $statements) as $variableName) { if (!isset($unusedParameters[$variableName])) { continue; @@ -43,10 +58,12 @@ public function getUnusedParameters( unset($unusedParameters[$variableName]); } $errors = []; - foreach (array_keys($unusedParameters) as $name) { - $errors[] = RuleErrorBuilder::message( - sprintf($unusedParameterMessage, $name), - )->identifier($identifier)->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; @@ -62,14 +79,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' || $functionName === 'get_defined_vars') { + 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 ( @@ -79,11 +96,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 c6324f7678..fb6c42086b 100644 --- a/src/Rules/Variables/CompactVariablesRule.php +++ b/src/Rules/Variables/CompactVariablesRule.php @@ -4,22 +4,28 @@ 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 Rule */ +#[RegisteredRule(level: 0)] final class CompactVariablesRule implements Rule { - public function __construct(private bool $checkMaybeUndefinedVariables) + public function __construct( + #[AutowiredParameter] + private bool $checkMaybeUndefinedVariables, + ) { } @@ -66,25 +72,24 @@ public function processNode(Node $node, Scope $scope): array } /** - * @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 fbe15dcfbc..8fbb1e5d0b 100644 --- a/src/Rules/Variables/DefinedVariableRule.php +++ b/src/Rules/Variables/DefinedVariableRule.php @@ -3,10 +3,16 @@ 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; @@ -14,11 +20,14 @@ /** * @implements Rule */ -class DefinedVariableRule implements Rule +#[RegisteredRule(level: 0)] +final class DefinedVariableRule implements Rule { public function __construct( + #[AutowiredParameter] private bool $cliArgumentsVariablesRegistered, + #[AutowiredParameter] private bool $checkMaybeUndefinedVariables, ) { @@ -31,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)) { @@ -49,18 +82,18 @@ public function processNode(Node $node, Scope $scope): array 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') ->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)) + RuleErrorBuilder::message(sprintf('Variable $%s might not be defined.', $variableName)) ->identifier('variable.undefined') ->build(), ]; diff --git a/src/Rules/Variables/EmptyRule.php b/src/Rules/Variables/EmptyRule.php index ca588bd711..d1656a3be2 100644 --- a/src/Rules/Variables/EmptyRule.php +++ b/src/Rules/Variables/EmptyRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Rule; use PHPStan\Type\Type; @@ -11,7 +12,8 @@ /** * @implements Rule */ -class EmptyRule implements Rule +#[RegisteredRule(level: 1)] +final class EmptyRule implements Rule { public function __construct(private IssetCheck $issetCheck) diff --git a/src/Rules/Variables/IssetRule.php b/src/Rules/Variables/IssetRule.php index f5c59363ff..839241a169 100644 --- a/src/Rules/Variables/IssetRule.php +++ b/src/Rules/Variables/IssetRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Rule; use PHPStan\Type\Type; @@ -11,7 +12,8 @@ /** * @implements Rule */ -class IssetRule implements Rule +#[RegisteredRule(level: 1)] +final class IssetRule implements Rule { public function __construct(private IssetCheck $issetCheck) diff --git a/src/Rules/Variables/NullCoalesceRule.php b/src/Rules/Variables/NullCoalesceRule.php index ef289640e3..79941d0293 100644 --- a/src/Rules/Variables/NullCoalesceRule.php +++ b/src/Rules/Variables/NullCoalesceRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Rule; use PHPStan\Type\Type; @@ -11,7 +12,8 @@ /** * @implements Rule */ -class NullCoalesceRule implements Rule +#[RegisteredRule(level: 1)] +final class NullCoalesceRule implements Rule { public function __construct(private IssetCheck $issetCheck) diff --git a/src/Rules/Variables/ParameterOutAssignedTypeRule.php b/src/Rules/Variables/ParameterOutAssignedTypeRule.php index 9c24597525..5bc0118057 100644 --- a/src/Rules/Variables/ParameterOutAssignedTypeRule.php +++ b/src/Rules/Variables/ParameterOutAssignedTypeRule.php @@ -4,9 +4,9 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\VariableAssignNode; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -20,7 +20,8 @@ /** * @implements Rule */ -class ParameterOutAssignedTypeRule implements Rule +#[RegisteredRule(level: 3)] +final class ParameterOutAssignedTypeRule implements Rule { public function __construct( @@ -50,8 +51,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $variant = ParametersAcceptorSelector::selectSingle($inFunction->getVariants()); - $parameters = $variant->getParameters(); + $parameters = $inFunction->getParameters(); $foundParameter = null; foreach ($parameters as $parameter) { if (!$parameter->passedByReference()->createsNewVariable()) { diff --git a/src/Rules/Variables/ParameterOutExecutionEndTypeRule.php b/src/Rules/Variables/ParameterOutExecutionEndTypeRule.php index 648781ccc5..edddff8a91 100644 --- a/src/Rules/Variables/ParameterOutExecutionEndTypeRule.php +++ b/src/Rules/Variables/ParameterOutExecutionEndTypeRule.php @@ -4,12 +4,12 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ExecutionEndNode; use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -24,7 +24,8 @@ /** * @implements Rule */ -class ParameterOutExecutionEndTypeRule implements Rule +#[RegisteredRule(level: 3)] +final class ParameterOutExecutionEndTypeRule implements Rule { public function __construct( @@ -57,12 +58,8 @@ public function processNode(Node $node, Scope $scope): array return []; } } - if ($endNode instanceof Node\Stmt\Throw_) { - return []; - } - $variant = ParametersAcceptorSelector::selectSingle($inFunction->getVariants()); - $parameters = $variant->getParameters(); + $parameters = $inFunction->getParameters(); $errors = []; foreach ($parameters as $parameter) { if (!$parameter->passedByReference()->createsNewVariable()) { @@ -83,7 +80,7 @@ public function processNode(Node $node, Scope $scope): array private function processSingleParameter( Scope $scope, FunctionReflection|ExtendedMethodReflection $inFunction, - ParameterReflectionWithPhpDocs $parameter, + ExtendedParameterReflection $parameter, ): array { $outType = $parameter->getOutType(); diff --git a/src/Rules/Variables/ThrowTypeRule.php b/src/Rules/Variables/ThrowTypeRule.php deleted file mode 100644 index 7678406a9e..0000000000 --- a/src/Rules/Variables/ThrowTypeRule.php +++ /dev/null @@ -1,62 +0,0 @@ - - */ -class ThrowTypeRule implements Rule -{ - - public function __construct( - private RuleLevelHelper $ruleLevelHelper, - ) - { - } - - public function getNodeType(): string - { - return Node\Stmt\Throw_::class; - } - - public function processNode(Node $node, Scope $scope): array - { - $throwableType = new ObjectType(Throwable::class); - $typeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $node->expr, - 'Throwing object of an unknown class %s.', - static fn (Type $type): bool => $throwableType->isSuperTypeOf($type)->yes(), - ); - - $foundType = $typeResult->getType(); - if ($foundType instanceof ErrorType) { - return $typeResult->getUnknownClassErrors(); - } - - $isSuperType = $throwableType->isSuperTypeOf($foundType); - if ($isSuperType->yes()) { - return []; - } - - return [ - RuleErrorBuilder::message(sprintf( - 'Invalid type %s to throw.', - $foundType->describe(VerbosityLevel::typeOnly()), - ))->identifier('throw.notThrowable')->build(), - ]; - } - -} diff --git a/src/Rules/Variables/UnsetRule.php b/src/Rules/Variables/UnsetRule.php index 43efa6646c..d2893e17fe 100644 --- a/src/Rules/Variables/UnsetRule.php +++ b/src/Rules/Variables/UnsetRule.php @@ -4,7 +4,10 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +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; @@ -14,9 +17,17 @@ /** * @implements Rule */ -class UnsetRule implements 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; @@ -28,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; diff --git a/src/Rules/Variables/VariableCloningRule.php b/src/Rules/Variables/VariableCloningRule.php index 89aca44aa9..bab0f8d995 100644 --- a/src/Rules/Variables/VariableCloningRule.php +++ b/src/Rules/Variables/VariableCloningRule.php @@ -6,6 +6,7 @@ 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; @@ -18,7 +19,8 @@ /** * @implements Rule */ -class VariableCloningRule implements Rule +#[RegisteredRule(level: 3)] +final class VariableCloningRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) diff --git a/src/Rules/Whitespace/FileWhitespaceRule.php b/src/Rules/Whitespace/FileWhitespaceRule.php index 7f3bcbdcfe..ee067b1ceb 100644 --- a/src/Rules/Whitespace/FileWhitespaceRule.php +++ b/src/Rules/Whitespace/FileWhitespaceRule.php @@ -3,10 +3,13 @@ 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; @@ -15,7 +18,8 @@ /** * @implements Rule */ -class FileWhitespaceRule implements Rule +#[RegisteredRule(level: 0)] +final class FileWhitespaceRule implements Rule { public function getNodeType(): string @@ -47,6 +51,7 @@ public function processNode(Node $node, Scope $scope): array /** * @return int|null */ + #[Override] public function enterNode(Node $node) { if ($node instanceof Node\Stmt\Declare_) { @@ -61,7 +66,7 @@ public function enterNode(Node $node) } return null; } - return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } /** 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 1009a53a0c..2d0b672f32 100644 --- a/src/Testing/ErrorFormatterTestCase.php +++ b/src/Testing/ErrorFormatterTestCase.php @@ -75,10 +75,6 @@ protected function getOutputContent(bool $decorated = false, bool $verbose = fal rewind($this->getOutputStream($decorated, $verbose)->getStream()); $contents = stream_get_contents($this->getOutputStream($decorated, $verbose)->getStream()); - if ($contents === false) { - throw new ShouldNotHappenException(); - } - return $this->rtrimMultiline($contents); } @@ -103,10 +99,10 @@ protected function getAnalysisResult(array|int $numFileErrors, int $numGenericEr $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, true, null, null, 'a tip'), + 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), - new Error('Foobar\\Buz', self::DIRECTORY_PATH . '/foo.php', 5, true, null, null, 'a tip', null, null, 'foobar.buz'), + new Error('Foobar\\Buz', self::DIRECTORY_PATH . '/foo.php', 5, tip: 'a tip', identifier: 'foobar.buz'), ], $offsetFileErrors, $numFileErrors); $genericErrors = array_slice([ diff --git a/src/Testing/LevelsTestCase.php b/src/Testing/LevelsTestCase.php index 7d0b1998a9..fa0eab7747 100644 --- a/src/Testing/LevelsTestCase.php +++ b/src/Testing/LevelsTestCase.php @@ -8,6 +8,7 @@ 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; @@ -30,7 +31,7 @@ abstract class LevelsTestCase extends TestCase /** * @return array> */ - abstract public function dataTopics(): array; + abstract public static function dataTopics(): array; abstract public function getDataPath(): string; @@ -48,9 +49,7 @@ protected function shouldAutoloadAnalysedFile(): bool return true; } - /** - * @dataProvider dataTopics - */ + #[DataProvider('dataTopics')] public function testLevels( string $topic, ): void @@ -71,7 +70,7 @@ public function testLevels( putenv('__PHPSTAN_FORCE_VALIDATE_STUB_FILES=1'); - foreach (range(0, 9) as $level) { + foreach (range(0, 10) as $level) { unset($outputLines); exec(sprintf('%s %s analyse --no-progress --error-format=prettyJson --level=%d %s %s %s', escapeshellarg(PHP_BINARY), $command, $level, $configPath !== null ? '--configuration ' . escapeshellarg($configPath) : '', $this->shouldAutoloadAnalysedFile() ? sprintf('--autoload-file %s', escapeshellarg($file)) : '', escapeshellarg($file)), $outputLines); 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 14efcc7480..7587ac8d05 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -5,15 +5,11 @@ use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\DirectInternalScopeFactory; use PHPStan\Analyser\Error; -use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\RicherScopeGetTypeHelper; use PHPStan\Analyser\ScopeFactory; use PHPStan\Analyser\TypeSpecifier; -use PHPStan\BetterReflection\Reflector\ClassReflector; -use PHPStan\BetterReflection\Reflector\ConstantReflector; -use PHPStan\BetterReflection\Reflector\FunctionReflector; use PHPStan\BetterReflection\Reflector\Reflector; -use PHPStan\Broker\Broker; use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\ContainerFactory; use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; @@ -25,9 +21,11 @@ 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; @@ -51,9 +49,6 @@ abstract class PHPStanTestCase extends TestCase { - /** @deprecated */ - public static bool $useStaticReflectionProvider = true; - /** @var array */ private static array $containers = []; @@ -116,15 +111,6 @@ public static function getParser(): Parser return $parser; } - /** - * @api - * @deprecated Use createReflectionProvider() instead - */ - public function createBroker(): Broker - { - return self::getContainer()->getByType(Broker::class); - } - /** @api */ public static function createReflectionProvider(): ReflectionProvider { @@ -136,19 +122,6 @@ public static function getReflector(): Reflector return self::getContainer()->getService('betterReflectionReflector'); } - /** - * @deprecated Use getReflector() instead. - * @return array{ClassReflector, FunctionReflector, ConstantReflector} - */ - public static function getReflectors(): array - { - return [ - self::getContainer()->getService('betterReflectionClassReflector'), - self::getContainer()->getService('betterReflectionFunctionReflector'), - self::getContainer()->getService('betterReflectionConstantReflector'), - ]; - } - public static function getClassReflectionExtensionRegistryProvider(): ClassReflectionExtensionRegistryProvider { return self::getContainer()->getByType(ClassReflectionExtensionRegistryProvider::class); @@ -166,20 +139,22 @@ public static function createScopeFactory(ReflectionProvider $reflectionProvider } $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider); - $constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames); + $composerPhpVersionFactory = $container->getByType(ComposerPhpVersionFactory::class); + $constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames, null, $composerPhpVersionFactory); + + $initializerExprTypeResolver = new InitializerExprTypeResolver( + $constantResolver, + $reflectionProviderProvider, + $container->getByType(PhpVersion::class), + $container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class), + new OversizedArrayBuilder(), + $container->getParameter('usePathConstantsAsConstantString'), + ); return new ScopeFactory( new DirectInternalScopeFactory( - MutatingScope::class, $reflectionProvider, - new InitializerExprTypeResolver( - $constantResolver, - $reflectionProviderProvider, - $container->getByType(PhpVersion::class), - $container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class), - new OversizedArrayBuilder(), - $container->getParameter('usePathConstantsAsConstantString'), - ), + $initializerExprTypeResolver, $container->getByType(DynamicReturnTypeExtensionRegistryProvider::class), $container->getByType(ExpressionTypeResolverExtensionRegistryProvider::class), $container->getByType(ExprPrinter::class), @@ -187,9 +162,10 @@ public static function createScopeFactory(ReflectionProvider $reflectionProvider new PropertyReflectionFinder(), self::getParser(), $container->getByType(NodeScopeResolver::class), + new RicherScopeGetTypeHelper($initializerExprTypeResolver), $container->getByType(PhpVersion::class), - $container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew'], - $container->getParameter('featureToggles')['explicitMixedForGlobalVariables'], + $container->getByType(AttributeReflectionFactory::class), + $container->getParameter('phpVersion'), $constantResolver, ), ); diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index 3b4330a297..36c383bc56 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -7,6 +7,7 @@ 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; @@ -19,21 +20,28 @@ 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\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 @@ -45,7 +53,7 @@ abstract class RuleTestCase extends PHPStanTestCase private ?Analyser $analyser = null; /** - * @phpstan-return TRule + * @return TRule */ abstract protected function getRule(): Rule; @@ -90,6 +98,8 @@ private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser 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, @@ -99,21 +109,21 @@ private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser self::createScopeFactory($reflectionProvider, $typeSpecifier), $this->shouldPolluteScopeWithLoopInitialAssignments(), $this->shouldPolluteScopeWithAlwaysIterableForeach(), + self::getContainer()->getParameter('polluteScopeWithBlock'), [], [], self::getContainer()->getParameter('universalObjectCratesClasses'), self::getContainer()->getParameter('exceptions')['implicitThrows'], $this->shouldTreatPhpDocTypesAsCertain(), - self::getContainer()->getParameter('featureToggles')['detectDeadTypeInMultiCatch'], - self::getContainer()->getParameter('featureToggles')['paramOutType'], - self::getContainer()->getParameter('featureToggles')['preciseMissingReturn'], + $this->shouldNarrowMethodScopeFromConstructor(), ); $fileAnalyser = new FileAnalyser( $this->createScopeFactory($reflectionProvider, $typeSpecifier), $nodeScopeResolver, $this->getParser(), self::getContainer()->getByType(DependencyResolver::class), - new RuleErrorTransformer(), + new IgnoreErrorExtensionProvider(self::getContainer()), + self::getContainer()->getByType(RuleErrorTransformer::class), new LocalIgnoresProcessor(), ); $this->analyser = new Analyser( @@ -134,7 +144,7 @@ private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser */ public function analyse(array $files, array $expectedErrors): void { - $actualErrors = $this->gatherAnalyserErrors($files); + [$actualErrors, $delayedErrors] = $this->gatherAnalyserErrorsWithDelayedErrors($files); $strictlyTypedSprintf = static function (int $line, string $message, ?string $tip): string { $message = sprintf('%02d: %s', $line, $message); if ($tip !== null) { @@ -160,7 +170,54 @@ static function (Error $error) use ($strictlyTypedSprintf): string { $actualErrors, ); - $this->assertSame(implode("\n", $expectedErrors) . "\n", implode("\n", $actualErrors) . "\n"); + $expectedErrorsString = implode("\n", $expectedErrors) . "\n"; + $actualErrorsString = implode("\n", $actualErrors) . "\n"; + + if (count($delayedErrors) === 0) { + $this->assertSame($expectedErrorsString, $actualErrorsString); + return; + } + + if ($expectedErrorsString === $actualErrorsString) { + $this->assertSame($expectedErrorsString, $actualErrorsString); + return; + } + + $actualErrorsString .= sprintf( + "\n%s might be reported because of the following misconfiguration %s:\n\n", + count($actualErrors) === 1 ? 'This error' : 'These errors', + count($delayedErrors) === 1 ? 'issue' : 'issues', + ); + + foreach ($delayedErrors as $delayedError) { + $actualErrorsString .= sprintf("* %s\n", $delayedError->getMessage()); + } + + $this->assertSame($expectedErrorsString, $actualErrorsString); + } + + 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); } /** @@ -169,8 +226,22 @@ static function (Error $error) use ($strictlyTypedSprintf): string { */ 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( @@ -192,18 +263,22 @@ public function gatherAnalyserErrors(array $files): array $finalizer = new AnalyserResultFinalizer( $ruleRegistry, - new RuleErrorTransformer(), - $this->createScopeFactory($this->createReflectionProvider(), $this->getTypeSpecifier()), + new IgnoreErrorExtensionProvider(self::getContainer()), + self::getContainer()->getByType(RuleErrorTransformer::class), + $this->createScopeFactory($reflectionProvider, $this->getTypeSpecifier()), new LocalIgnoresProcessor(), true, ); - return $finalizer->finalize($analyserResult, false, true)->getAnalyserResult()->getUnorderedErrors(); + 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 @@ -216,6 +291,11 @@ protected function shouldFailOnPhpErrors(): bool return true; } + protected function shouldNarrowMethodScopeFromConstructor(): bool + { + return false; + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/src/Testing/TestCase.neon b/src/Testing/TestCase.neon index cfa21959b9..2682e8dac3 100644 --- a/src/Testing/TestCase.neon +++ b/src/Testing/TestCase.neon @@ -1,7 +1,5 @@ parameters: inferPrivatePropertyTypeFromConstructor: true - featureToggles: - checkUnresolvableParameterTypes: true services: - @@ -10,25 +8,29 @@ services: phpParser: @phpParserDecorator php8Parser: @php8PhpParser fileExtensions: %fileExtensions% - obsoleteExcludesAnalyse: %excludes_analyse% 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!: [] diff --git a/src/Testing/TestCaseSourceLocatorFactory.php b/src/Testing/TestCaseSourceLocatorFactory.php index fccedd9ec1..a87bf4d9df 100644 --- a/src/Testing/TestCaseSourceLocatorFactory.php +++ b/src/Testing/TestCaseSourceLocatorFactory.php @@ -16,14 +16,16 @@ 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; use ReflectionClass; use function dirname; use function is_file; use function serialize; use function sha1; +use const PHP_VERSION_ID; -class TestCaseSourceLocatorFactory +final class TestCaseSourceLocatorFactory { /** @var array> */ @@ -31,11 +33,11 @@ class TestCaseSourceLocatorFactory /** * @param string[] $fileExtensions - * @param string[] $obsoleteExcludesAnalyse * @param array{analyse?: array, analyseAndScan?: array}|null $excludePaths */ public function __construct( private ComposerJsonAndInstalledJsonSourceLocatorMaker $composerJsonAndInstalledJsonSourceLocatorMaker, + private OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, private Parser $phpParser, private Parser $php8Parser, private FileNodesFetcher $fileNodesFetcher, @@ -43,7 +45,6 @@ public function __construct( private ReflectionSourceStubber $reflectionSourceStubber, private PhpVersion $phpVersion, private array $fileExtensions, - private array $obsoleteExcludesAnalyse, private ?array $excludePaths, ) { @@ -56,11 +57,16 @@ public function create(): SourceLocator $cacheKey = sha1(serialize([ $this->phpVersion->getVersionId(), $this->fileExtensions, - $this->obsoleteExcludesAnalyse, $this->excludePaths, ])); if ($classLoaderReflection->hasProperty('vendorDir') && ! isset(self::$composerSourceLocatorsCache[$cacheKey])) { - $composerLocators = []; + $composerLocators = [ + $this->optimizedSingleFileSourceLocatorRepository->getOrCreate( + PHP_VERSION_ID < 80500 + ? __DIR__ . '/../../stubs/runtime/Attribute84.php' + : __DIR__ . '/../../stubs/runtime/Attribute85.php', + ), + ]; $vendorDirProperty = $classLoaderReflection->getProperty('vendorDir'); $vendorDirProperty->setAccessible(true); foreach ($classLoaders as $classLoader) { diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index c9c32db584..7c50ebaf5c 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -13,12 +13,18 @@ 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; @@ -36,8 +42,8 @@ use function is_string; use function preg_match; use function sprintf; +use function str_starts_with; use function stripos; -use function strpos; use function strtolower; use function version_compare; use const PHP_VERSION; @@ -70,6 +76,8 @@ public static function processFile( 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, @@ -79,14 +87,13 @@ public static function processFile( 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'), - self::getContainer()->getParameter('featureToggles')['detectDeadTypeInMultiCatch'], - self::getContainer()->getParameter('featureToggles')['paramOutType'], - self::getContainer()->getParameter('featureToggles')['preciseMissingReturn'], + true, ); $resolver->setAnalysedFiles(array_map(static fn (string $file): string => $fileHelper->normalizePath($file), array_merge([$file], static::getAdditionalAnalysedFiles()))); @@ -123,18 +130,66 @@ public function assertFileAsserts( $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 in %s on line %d.', $expectedCertainty->describe(), $variableName, $actualCertainty->describe(), $file, $args[3]), + $failureMessage, ); } } @@ -148,11 +203,31 @@ 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 = []; - self::processFile($file, static function (Node $node, Scope $scope) use (&$asserts, $file, $relativePathHelper): 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; } @@ -163,7 +238,7 @@ public static function gatherAssertTypes(string $file): array } $functionName = $nameNode->toString(); - if (in_array(strtolower($functionName), ['asserttype', 'assertnativetype', 'assertvariablecertainty'], true)) { + if (in_array(strtolower($functionName), ['asserttype', 'assertnativetype', 'assertsupertype', 'assertvariablecertainty'], true)) { self::fail(sprintf( 'Missing use statement for %s() in %s on line %d.', $functionName, @@ -177,7 +252,7 @@ public static function gatherAssertTypes(string $file): array 'Expected type must be a literal string, %s given in %s on line %d.', $expectedType->describe(VerbosityLevel::precise()), $relativePathHelper->getRelativePath($file), - $node->getLine(), + $node->getStartLine(), )); } $actualType = $scope->getType($node->getArgs()[1]->value); @@ -189,12 +264,28 @@ public static function gatherAssertTypes(string $file): array 'Expected type must be a literal string, %s given in %s on line %d.', $expectedType->describe(VerbosityLevel::precise()), $relativePathHelper->getRelativePath($file), - $node->getLine(), + $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) { @@ -215,21 +306,25 @@ public static function gatherAssertTypes(string $file): array // @phpstan-ignore staticMethod.dynamicName $expectedertaintyValue = TrinaryLogic::{$certainty->name->toString()}(); $variable = $node->getArgs()[1]->value; - if (!$variable instanceof Node\Expr\Variable) { - self::fail(sprintf('ERROR: Invalid assertVariableCertainty call.')); - } - 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 { self::fail(sprintf('ERROR: Invalid assertVariableCertainty call.')); } - $actualCertaintyValue = $scope->hasVariableType($variable->name); - $assert = ['variableCertainty', $file, $expectedertaintyValue, $actualCertaintyValue, $variable->name, $node->getStartLine()]; + $assert = ['variableCertainty', $file, $expectedertaintyValue, $actualCertaintyValue, $variableDescription, $node->getStartLine()]; } else { $correctFunction = null; $assertFunctions = [ 'assertType' => 'PHPStan\\Testing\\assertType', 'assertNativeType' => 'PHPStan\\Testing\\assertNativeType', + 'assertSuperType' => 'PHPStan\\Testing\\assertSuperType', 'assertVariableCertainty' => 'PHPStan\\Testing\\assertVariableCertainty', ]; foreach ($assertFunctions as $assertFn => $fqFunctionName) { @@ -269,6 +364,15 @@ public static function gatherAssertTypes(string $file): array 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; } @@ -277,6 +381,21 @@ public static function gatherAssertTypes(string $file): array * @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)); @@ -284,18 +403,16 @@ public static function gatherAssertTypesFromDirectory(string $directory): array $finder = new Finder(); $finder->followLinks(); - $asserts = []; + $files = []; foreach ($finder->files()->name('*.php')->in($directory) as $fileInfo) { $path = $fileInfo->getPathname(); if (self::isFileLintSkipped($path)) { continue; } - foreach (self::gatherAssertTypes($path) as $key => $assert) { - $asserts[$key] = $assert; - } + $files[] = $path; } - return $asserts; + return $files; } /** @@ -313,7 +430,7 @@ private static function isFileLintSkipped(string $file): bool } // ignore shebang line - if (strpos($firstLine, '#!') === 0) { + if (str_starts_with($firstLine, '#!')) { $firstLine = fgets($f); if ($firstLine === false) { return false; diff --git a/src/Testing/functions.php b/src/Testing/functions.php index c79cc2d99c..241d78d31f 100644 --- a/src/Testing/functions.php +++ b/src/Testing/functions.php @@ -35,6 +35,20 @@ function assertNativeType(string $type, $value) // phpcs:ignore return null; } +/** + * Asserts a super type of a value. + * + * @phpstan-pure + * @param mixed $value + * @return mixed + * + * @throws void + */ +function assertSuperType(string $superType, $value) // phpcs:ignore +{ + return null; +} + /** * @phpstan-pure * @param mixed $variable diff --git a/src/TrinaryLogic.php b/src/TrinaryLogic.php index df4e9f5218..538f81fc66 100644 --- a/src/TrinaryLogic.php +++ b/src/TrinaryLogic.php @@ -12,7 +12,7 @@ * @api * @see https://phpstan.org/developing-extensions/trinary-logic */ -class TrinaryLogic +final class TrinaryLogic { private const YES = 1; @@ -79,9 +79,15 @@ 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); } /** @@ -251,12 +257,4 @@ public function describe(): string return $labels[$this->value]; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self - { - return self::create($properties['value']); - } - } diff --git a/src/Type/AcceptsResult.php b/src/Type/AcceptsResult.php index e6e5df0f9b..2358285f16 100644 --- a/src/Type/AcceptsResult.php +++ b/src/Type/AcceptsResult.php @@ -9,8 +9,10 @@ use function array_unique; use function array_values; -/** @api */ -class AcceptsResult +/** + * @api + */ +final class AcceptsResult { /** @@ -105,7 +107,7 @@ public static function extremeIdentity(self ...$operands): self } } - return new self($result, $reasons); + return new self($result, array_values(array_unique($reasons))); } public static function maxMin(self ...$operands): self @@ -122,7 +124,7 @@ public static function maxMin(self ...$operands): self } } - return new self($result, $reasons); + return new self($result, array_values(array_unique($reasons))); } } diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index 874b177716..6ed6baeced 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -14,6 +14,7 @@ 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; @@ -39,8 +40,7 @@ class AccessoryArrayListType implements CompoundType, AccessoryType use NonRemoveableTypeTrait; use NonGeneralizableTypeTrait; - private static bool $enabled = false; - + /** @api */ public function __construct() { } @@ -75,15 +75,10 @@ public function getConstantStrings(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } $isArray = $type->isArray(); @@ -92,39 +87,32 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult return new AcceptsResult($isArray->and($isList), []); } - 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->isList()); + return new IsSuperTypeOfResult($type->isArray()->and($type->isList()), []); } - 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->isList()) - ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + return (new IsSuperTypeOfResult($otherType->isArray()->and($otherType->isList()), [])) + ->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->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; - } - - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult - { - return new AcceptsResult($this->isSubTypeOf($acceptingType), []); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -149,7 +137,7 @@ public function isOffsetAccessLegal(): TrinaryLogic public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - return $this->getIterableKeyType()->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + return $this->getIterableKeyType()->isSuperTypeOf($offsetType)->result->and(TrinaryLogic::createMaybe()); } public function getOffsetValueType(Type $offsetType): Type @@ -168,11 +156,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()) { - return $this; - } - - return new ErrorType(); + return $this; } public function unsetOffset(Type $offsetType): Type @@ -184,6 +168,11 @@ 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; @@ -194,6 +183,11 @@ 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(); @@ -218,7 +212,16 @@ public function popArray(): Type return $this; } - public function searchArray(Type $needleType): Type + 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(); } @@ -233,6 +236,24 @@ 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(); @@ -373,7 +394,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -444,38 +475,19 @@ public function toArrayKey(): Type return new ErrorType(); } - public function traverse(callable $cb): Type + public function toCoercedArgumentType(bool $strictTypes): Type { return $this; } - public function traverseSimultaneously(Type $right, callable $cb): Type + public function traverse(callable $cb): Type { return $this; } - public static function __set_state(array $properties): Type - { - return new self(); - } - - public static function setListTypeEnabled(bool $enabled): void - { - self::$enabled = $enabled; - } - - public static function isListTypeEnabled(): bool - { - return self::$enabled; - } - - public static function intersectWith(Type $type): Type + public function traverseSimultaneously(Type $right, callable $cb): Type { - if (self::$enabled) { - return TypeCombinator::intersect($type, new self()); - } - - return $type; + return $this; } public function exponentiate(Type $exponent): Type diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index da1a00a16a..d8a02005d6 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -17,6 +17,7 @@ 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; @@ -28,6 +29,7 @@ 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; @@ -67,54 +69,44 @@ public function getConstantStrings(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof MixedType) { return AcceptsResult::createNo(); } if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } 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->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; - } - - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult - { - return new AcceptsResult($this->isSubTypeOf($acceptingType), []); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -153,6 +145,12 @@ 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; } @@ -206,8 +204,7 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], - [], - TrinaryLogic::createYes(), + isList: TrinaryLogic::createYes(), ); } @@ -216,6 +213,15 @@ 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(); @@ -291,7 +297,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createYes(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } @@ -341,11 +357,6 @@ public function generalize(GeneralizePrecision $precision): Type return new StringType(); } - public static function __set_state(array $properties): Type - { - return new self(); - } - public function exponentiate(Type $exponent): Type { return new BenevolentUnionType([ 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 8e98af9d78..a8b573809a 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -11,6 +11,7 @@ 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; @@ -18,6 +19,7 @@ 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; @@ -68,55 +70,48 @@ public function getConstantStrings(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { + if ($type->isNonEmptyString()->yes()) { + return AcceptsResult::createYes(); + } if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return new AcceptsResult($type->isNonEmptyString(), []); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } if ($this->equals($type)) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type->isNonFalsyString()->yes()) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return $type->isNonEmptyString(); + return new IsSuperTypeOfResult($type->isNonEmptyString(), []); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } - return $otherType->isNonEmptyString() - ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + return (new IsSuperTypeOfResult($otherType->isNonEmptyString(), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; - } - - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult - { - return new AcceptsResult($this->isSubTypeOf($acceptingType), []); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -159,6 +154,12 @@ 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; } @@ -203,8 +204,7 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], - [], - TrinaryLogic::createYes(), + isList: TrinaryLogic::createYes(), ); } @@ -213,6 +213,15 @@ 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(); @@ -288,7 +297,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } @@ -315,6 +334,14 @@ public function isScalar(): TrinaryLogic 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(); } @@ -333,11 +360,6 @@ public function generalize(GeneralizePrecision $precision): Type return new StringType(); } - public static function __set_state(array $properties): Type - { - return new self(); - } - public function tryRemove(Type $typeToRemove): ?Type { if ($typeToRemove instanceof ConstantStringType && $typeToRemove->getValue() === '0') { diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php index 460137066c..f32d613154 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -11,13 +11,16 @@ 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\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\StaticTypeFactory; use PHPStan\Type\StringType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; use PHPStan\Type\Traits\NonArrayTypeTrait; @@ -28,6 +31,7 @@ use PHPStan\Type\Traits\TruthyBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -68,55 +72,45 @@ public function getConstantStrings(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return new AcceptsResult($type->isNonFalsyString(), []); } - 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->isNonFalsyString(); + return new IsSuperTypeOfResult($type->isNonFalsyString(), []); } - 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 AccessoryNonEmptyStringType) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return $otherType->isNonFalsyString() - ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + return (new IsSuperTypeOfResult($otherType->isNonFalsyString(), [])) + ->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->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; - } - - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult - { - return new AcceptsResult($this->isSubTypeOf($acceptingType), []); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -155,6 +149,12 @@ 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->isNonFalsyString()->yes()) { return $this; } @@ -184,6 +184,7 @@ public function toAbsoluteNumber(): Type public function toInteger(): Type { + // Do not remove `0` since `(int) '00'` is still `0`. return new IntegerType(); } @@ -203,8 +204,7 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], - [], - TrinaryLogic::createYes(), + isList: TrinaryLogic::createYes(), ); } @@ -213,6 +213,15 @@ 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(); @@ -288,7 +297,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } @@ -315,6 +334,11 @@ public function isScalar(): TrinaryLogic public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { + $falseyTypes = StaticTypeFactory::falsey(); + if ($falseyTypes->isSuperTypeOf($type)->yes()) { + return new ConstantBooleanType(false); + } + return new BooleanType(); } @@ -333,11 +357,6 @@ public function generalize(GeneralizePrecision $precision): Type return new StringType(); } - public static function __set_state(array $properties): Type - { - return new self(); - } - public function exponentiate(Type $exponent): Type { return new BenevolentUnionType([ diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 292d00f7a0..c10dd28082 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -11,6 +11,7 @@ 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; @@ -18,6 +19,7 @@ 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; @@ -67,49 +69,39 @@ public function getConstantStrings(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } 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 - { - return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; - } - - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { if ($acceptingType->isNonFalsyString()->yes()) { return AcceptsResult::createMaybe(); @@ -119,7 +111,7 @@ public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): return AcceptsResult::createYes(); } - return new AcceptsResult($this->isSubTypeOf($acceptingType), []); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -158,6 +150,12 @@ 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; } @@ -205,14 +203,28 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], - [], - TrinaryLogic::createYes(), + isList: TrinaryLogic::createYes(), ); } public function toArrayKey(): Type { - return new IntegerType(); + 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 @@ -290,7 +302,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -317,6 +339,14 @@ public function isScalar(): TrinaryLogic 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(); } @@ -335,11 +365,6 @@ public function generalize(GeneralizePrecision $precision): Type return new StringType(); } - public static function __set_state(array $properties): Type - { - return new self(); - } - public function tryRemove(Type $typeToRemove): ?Type { if ($typeToRemove instanceof ConstantStringType && $typeToRemove->getValue() === '0') { 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 98503a13a8..b59a77c788 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -15,6 +15,7 @@ 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; @@ -61,52 +62,42 @@ private function getCanonicalMethodName(): string return strtolower($this->methodName); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + 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 TrinaryLogic::createYes(); + 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)); - } - - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic - { - return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + return $limit->and(new IsSuperTypeOfResult($otherType->hasMethod($this->methodName), [])); } - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return new AcceptsResult($this->isSubTypeOf($acceptingType), []); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -120,11 +111,6 @@ public function describe(VerbosityLevel $level): string return sprintf('hasMethod(%s)', $this->methodName); } - public function isOffsetAccessLegal(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - public function hasMethod(string $methodName): TrinaryLogic { if ($this->getCanonicalMethodName() === strtolower($methodName)) { @@ -200,11 +186,6 @@ public function getFiniteTypes(): array return []; } - public static function __set_state(array $properties): Type - { - return new self($properties['methodName']); - } - public function toPhpDocNode(): TypeNode { return new IdentifierTypeNode(''); // no PHPDoc representation diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index cdb258d613..da6c176655 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -12,7 +12,9 @@ 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; @@ -25,6 +27,7 @@ use PHPStan\Type\Traits\TruthyBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -44,9 +47,8 @@ class HasOffsetType implements CompoundType, AccessoryType /** * @api - * @param ConstantStringType|ConstantIntegerType $offsetType */ - public function __construct(private Type $offsetType) + public function __construct(private ConstantStringType|ConstantIntegerType $offsetType) { } @@ -78,48 +80,38 @@ public function getConstantStrings(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } 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)), []); - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic - { - return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + return $result + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); } - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return new AcceptsResult($this->isSubTypeOf($acceptingType), []); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -175,6 +167,11 @@ public function unsetOffset(Type $offsetType): Type return $this; } + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return new NonEmptyArrayType(); + } + public function fillKeysArray(Type $valueType): Type { return new NonEmptyArrayType(); @@ -189,11 +186,43 @@ public function intersectKeyArray(Type $otherArraysType): Type 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(); @@ -283,7 +312,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } @@ -313,6 +352,11 @@ 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(); @@ -358,6 +402,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + public function getEnumCases(): array { return []; @@ -383,11 +432,6 @@ public function getFiniteTypes(): array return []; } - public static function __set_state(array $properties): Type - { - return new self($properties['offsetType']); - } - public function toPhpDocNode(): TypeNode { return new IdentifierTypeNode(''); // no PHPDoc representation diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index a87f7879ab..5e7c834942 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -14,9 +14,13 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ErrorType; +use PHPStan\Type\IntegerRangeType; +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\MaybeArrayTypeTrait; use PHPStan\Type\Traits\MaybeCallableTypeTrait; use PHPStan\Type\Traits\MaybeIterableTypeTrait; @@ -27,6 +31,7 @@ use PHPStan\Type\Traits\TruthyBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -78,55 +83,48 @@ public function getConstantStrings(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return new AcceptsResult( $type->isOffsetAccessible() ->and($type->hasOffsetValueType($this->offsetType)) - ->and($this->valueType->accepts($type->getOffsetValueType($this->offsetType), $strictTypes)), + ->and($this->valueType->accepts($type->getOffsetValueType($this->offsetType), $strictTypes)->result), [], ); } - 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)) + + $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): 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->getOffsetValueType($this->offsetType)->isSuperTypeOf($this->valueType)) - ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); - } + $result = new IsSuperTypeOfResult($otherType->isOffsetAccessible()->and($otherType->hasOffsetValueType($this->offsetType)), []); - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic - { - return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + return $result + ->and($otherType->getOffsetValueType($this->offsetType)->isSuperTypeOf($this->valueType)) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); } - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return new AcceptsResult($this->isSubTypeOf($acceptingType), []); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -188,6 +186,10 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { + if (!$offsetType->equals($this->offsetType)) { + return $this; + } + return new self($this->offsetType, $valueType); } @@ -199,6 +201,11 @@ public function unsetOffset(Type $offsetType): Type return $this; } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + public function getKeysArray(): Type { return new NonEmptyArrayType(); @@ -209,6 +216,11 @@ 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(); @@ -233,13 +245,30 @@ public function intersectKeyArray(Type $otherArraysType): Type return new MixedType(); } - public function searchArray(Type $needleType): Type + 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() + && ( + $needleType->getValue() === $this->valueType->getValue() + // @phpstan-ignore equal.notAllowed + || ($strict->no() && $needleType->getValue() == $this->valueType->getValue()) // phpcs:ignore + ) ) { - return $this->offsetType; + return new UnionType([ + new IntegerType(), + new StringType(), + ]); } return new MixedType(); @@ -250,6 +279,29 @@ 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(); @@ -339,7 +391,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } @@ -404,6 +466,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + public function getEnumCases(): array { return []; @@ -439,11 +506,6 @@ public function getFiniteTypes(): array return []; } - public static function __set_state(array $properties): Type - { - return new self($properties['offsetType'], $properties['valueType']); - } - public function toPhpDocNode(): TypeNode { return new IdentifierTypeNode(''); // no PHPDoc representation diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index f508447135..0c2ca8e47a 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -11,6 +11,7 @@ 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; @@ -35,9 +36,6 @@ public function __construct(private string $propertyName) { } - /** - * @return string[] - */ public function getReferencedClasses(): array { return []; @@ -63,48 +61,38 @@ public function getPropertyName(): string return $this->propertyName; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return AcceptsResult::createFromBoolean($this->equals($type)); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - return $type->hasProperty($this->propertyName); + return new IsSuperTypeOfResult($type->hasProperty($this->propertyName), []); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } if ($otherType instanceof self) { - $limit = TrinaryLogic::createYes(); + $limit = IsSuperTypeOfResult::createYes(); } else { - $limit = TrinaryLogic::createMaybe(); + $limit = IsSuperTypeOfResult::createMaybe(); } - return $limit->and($otherType->hasProperty($this->propertyName)); + return $limit->and(new IsSuperTypeOfResult($otherType->hasProperty($this->propertyName), [])); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; - } - - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult - { - return new AcceptsResult($this->isSubTypeOf($acceptingType), []); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -118,11 +106,6 @@ public function describe(VerbosityLevel $level): string return sprintf('hasProperty(%s)', $this->propertyName); } - public function isOffsetAccessLegal(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - public function hasProperty(string $propertyName): TrinaryLogic { if ($this->propertyName === $propertyName) { @@ -162,11 +145,6 @@ public function getFiniteTypes(): array return []; } - public static function __set_state(array $properties): Type - { - return new self($properties['propertyName']); - } - public function toPhpDocNode(): TypeNode { return new IdentifierTypeNode(''); // no PHPDoc representation diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index 8ef7ecbb88..d360d85172 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -9,11 +9,13 @@ 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; @@ -72,15 +74,10 @@ public function getConstantStrings(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } $isArray = $type->isArray(); @@ -89,39 +86,32 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult 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()); - } - - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic - { - return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + return (new IsSuperTypeOfResult($otherType->isArray()->and($otherType->isIterableAtLeastOnce()), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); } - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return new AcceptsResult($this->isSubTypeOf($acceptingType), []); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -169,6 +159,11 @@ 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; @@ -179,6 +174,11 @@ public function getValuesArray(): Type return $this; } + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this; + } + public function fillKeysArray(Type $valueType): Type { return $this; @@ -199,7 +199,12 @@ public function popArray(): Type return new MixedType(); } - public function searchArray(Type $needleType): Type + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this; + } + + public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type { return new MixedType(); } @@ -214,6 +219,27 @@ 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(); @@ -354,7 +380,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -381,6 +417,10 @@ public function isScalar(): TrinaryLogic public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { + if ($type->isArray()->yes() && $type->isIterableAtLeastOnce()->no()) { + return new ConstantBooleanType(false); + } + return new BooleanType(); } @@ -419,6 +459,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + public function traverse(callable $cb): Type { return $this; @@ -439,11 +484,6 @@ public function getFiniteTypes(): array return []; } - public static function __set_state(array $properties): Type - { - return new self(); - } - public function toPhpDocNode(): TypeNode { return new IdentifierTypeNode('non-empty-array'); diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index c6c9c5b6d7..77fdec48fb 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -14,6 +14,7 @@ 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; @@ -71,53 +72,41 @@ public function getConstantStrings(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return new AcceptsResult($type->isArray()->and($type->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->isOversizedArray()); + return new IsSuperTypeOfResult($type->isArray()->and($type->isOversizedArray()), []); } - 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->isOversizedArray()) - ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); - } - - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic - { - return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + return (new IsSuperTypeOfResult($otherType->isArray()->and($otherType->isOversizedArray()), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); } - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return new AcceptsResult($this->isSubTypeOf($acceptingType), []); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -165,6 +154,11 @@ 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; @@ -175,6 +169,11 @@ public function getValuesArray(): Type return $this; } + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this; + } + public function fillKeysArray(Type $valueType): Type { return $this; @@ -195,7 +194,12 @@ public function popArray(): Type return $this; } - public function searchArray(Type $needleType): Type + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this; + } + + public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type { return new MixedType(); } @@ -210,6 +214,16 @@ 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(); @@ -217,12 +231,12 @@ public function isIterable(): TrinaryLogic public function isIterableAtLeastOnce(): TrinaryLogic { - return TrinaryLogic::createYes(); + return TrinaryLogic::createMaybe(); } public function getArraySize(): Type { - return IntegerRangeType::fromInterval(1, null); + return IntegerRangeType::fromInterval(0, null); } public function getIterableKeyType(): Type @@ -350,7 +364,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -415,6 +439,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + public function traverse(callable $cb): Type { return $this; @@ -435,11 +464,6 @@ public function getFiniteTypes(): array return []; } - public static function __set_state(array $properties): Type - { - return new self(); - } - public function toPhpDocNode(): TypeNode { return new IdentifierTypeNode(''); // no PHPDoc representation diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 6040f0ee06..80cfc6b09f 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -11,7 +11,6 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; -use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Constant\ConstantArrayType; @@ -21,8 +20,10 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\TemplateMixedType; +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; @@ -36,6 +37,7 @@ class ArrayType implements Type { + use ArrayTypeTrait; use MaybeCallableTypeTrait; use NonObjectTypeTrait; use UndecidedBooleanTypeTrait; @@ -50,6 +52,10 @@ public function __construct(Type $keyType, private Type $itemType) if ($keyType->describe(VerbosityLevel::value()) === '(int|string)') { $keyType = new MixedType(); } + if ($keyType instanceof StrictMixedType && !$keyType instanceof TemplateStrictMixedType) { + $keyType = new UnionType([new StringType(), new IntegerType()]); + } + $this->keyType = $keyType; } @@ -63,9 +69,6 @@ public function getItemType(): Type return $this->itemType; } - /** - * @return string[] - */ public function getReferencedClasses(): array { return array_merge( @@ -74,35 +77,15 @@ public function getReferencedClasses(): array ); } - public function getObjectClassNames(): array - { - return []; - } - - public function getObjectClassReflections(): array - { - return []; - } - - public function getArrays(): array - { - return [$this]; - } - public function getConstantArrays(): array { return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } if ($type instanceof ConstantArrayType) { @@ -111,8 +94,8 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult $itemType = $this->getItemType(); foreach ($type->getKeyTypes() as $i => $keyType) { $valueType = $type->getValueTypes()[$i]; - $acceptsKey = $thisKeyType->acceptsWithReason($keyType, $strictTypes); - $acceptsValue = $itemType->acceptsWithReason($valueType, $strictTypes); + $acceptsKey = $thisKeyType->accepts($keyType, $strictTypes); + $acceptsValue = $itemType->accepts($valueType, $strictTypes); $result = $result->and($acceptsKey)->and($acceptsValue); } @@ -120,16 +103,16 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult } if ($type instanceof ArrayType) { - return $this->getItemType()->acceptsWithReason($type->getItemType(), $strictTypes) - ->and($this->keyType->acceptsWithReason($type->keyType, $strictTypes)); + return $this->getItemType()->accepts($type->getItemType(), $strictTypes) + ->and($this->keyType->accepts($type->keyType, $strictTypes)); } 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->getIterableKeyType()->isSuperTypeOf($type->getIterableKeyType())); } @@ -138,21 +121,20 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool { return $type instanceof self - && $type->isConstantArray()->no() && $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) { @@ -183,32 +165,24 @@ function () use ($level, $isMixedKeyType, $isMixedItemType): string { ); } - /** - * @deprecated - */ - public function generalizeKeys(): self - { - return new self($this->keyType->generalize(GeneralizePrecision::lessSpecific()), $this->itemType); - } - public function generalizeValues(): self { return new self($this->keyType, $this->itemType->generalize(GeneralizePrecision::lessSpecific())); } - public function getKeysArray(): Type + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type { - return AccessoryArrayListType::intersectWith(new self(new IntegerType(), $this->getIterableKeyType())); + return $this->getKeysArray(); } - public function getValuesArray(): Type + public function getKeysArray(): Type { - return AccessoryArrayListType::intersectWith(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 @@ -259,33 +233,22 @@ public function getLastIterableValueType(): Type return $this->getItemType(); } - public function isArray(): TrinaryLogic - { - return TrinaryLogic::createYes(); - } - public function isConstantArray(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function isOversizedArray(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - public function isList(): TrinaryLogic { if (IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($this->getKeyType())->no()) { return TrinaryLogic::createNo(); } - return TrinaryLogic::createMaybe(); - } + if ($this->getKeyType()->isSuperTypeOf(new ConstantIntegerType(0))->no()) { + return TrinaryLogic::createNo(); + } - public function isNull(): TrinaryLogic - { - return TrinaryLogic::createNo(); + return TrinaryLogic::createMaybe(); } public function isConstantValue(): TrinaryLogic @@ -293,109 +256,13 @@ public function isConstantValue(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isConstantScalarValue(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function getConstantScalarTypes(): array - { - return []; - } - - public function getConstantScalarValues(): array - { - return []; - } - - public function isTrue(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function isFalse(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function isBoolean(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function isFloat(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function isInteger(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function isString(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function isNumericString(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function isNonEmptyString(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function isNonFalsyString(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function isLiteralString(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function isClassStringType(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function getClassStringObjectType(): Type - { - return new ErrorType(); - } - - public function getObjectTypeOrClassStringObjectType(): Type - { - return new ErrorType(); - } - - public function isVoid(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function isScalar(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { - return new BooleanType(); - } - - public function isOffsetAccessible(): TrinaryLogic - { - return TrinaryLogic::createYes(); - } + if ($type->isInteger()->yes()) { + return new ConstantBooleanType(false); + } - public function isOffsetAccessLegal(): TrinaryLogic - { - return TrinaryLogic::createYes(); + return new BooleanType(); } public function hasOffsetValueType(Type $offsetType): TrinaryLogic @@ -435,7 +302,17 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni if ($isKeyTypeInteger->no()) { $offsetType = new IntegerType(); } elseif ($isKeyTypeInteger->yes()) { - $offsetType = $this->keyType; + /** @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 { @@ -552,8 +429,18 @@ public function popArray(): Type return $this; } - public function searchArray(Type $needleType): Type + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this; + } + + public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type { + $strict ??= TrinaryLogic::createMaybe(); + if ($strict->yes() && $this->getIterableValueType()->isSuperTypeOf($needleType)->no()) { + return new ConstantBooleanType(false); + } + return TypeCombinator::union($this->getIterableKeyType(), new ConstantBooleanType(false)); } @@ -564,36 +451,55 @@ public function shiftArray(): Type public function shuffleArray(): Type { - return AccessoryArrayListType::intersectWith(new self(new IntegerType(), $this->itemType)); + return TypeCombinator::intersect(new self(new IntegerType(), $this->itemType), new AccessoryArrayListType()); } - public function isCallable(): TrinaryLogic + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { - return TrinaryLogic::createMaybe()->and($this->itemType->isString()); - } + if ((new ConstantIntegerType(0))->isSuperTypeOf($lengthType)->yes()) { + return new ConstantArrayType([], []); + } - public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array - { - if ($this->isCallable()->no()) { - throw new ShouldNotHappenException(); + if ($preserveKeys->no() && $this->keyType->isInteger()->yes()) { + return TypeCombinator::intersect(new self(new IntegerType(), $this->itemType), new AccessoryArrayListType()); } - return [new TrivialParametersAcceptor()]; + return $this; } - public function toNumber(): Type + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type { - return new ErrorType(); + $replacementArrayType = $replacementType->toArray(); + $replacementArrayTypeIsIterableAtLeastOnce = $replacementArrayType->isIterableAtLeastOnce(); + + 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 toAbsoluteNumber(): Type + public function isCallable(): TrinaryLogic { - return new ErrorType(); + return TrinaryLogic::createMaybe()->and($this->itemType->isString()); } - public function toString(): Type + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { - return new ErrorType(); + if ($this->isCallable()->no()) { + throw new ShouldNotHappenException(); + } + + return [new TrivialParametersAcceptor()]; } public function toInteger(): Type @@ -612,28 +518,6 @@ public function toFloat(): Type ); } - public function toArray(): Type - { - return $this; - } - - public function toArrayKey(): Type - { - return new ErrorType(); - } - - /** @deprecated Use getArraySize() instead */ - public function count(): Type - { - return $this->getArraySize(); - } - - /** @deprecated Use $offsetType->toArrayKey() instead */ - public static function castToArrayKeyType(Type $offsetType): Type - { - return $offsetType->toArrayKey(); - } - public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) { @@ -678,8 +562,8 @@ public function traverse(callable $cb): Type 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'; + $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) { @@ -729,36 +613,12 @@ public function tryRemove(Type $typeToRemove): ?Type return new ConstantArrayType([], []); } - if ($this->isConstantArray()->yes() && $typeToRemove instanceof HasOffsetType) { - return $this->unsetOffset($typeToRemove->getOffsetType()); - } - - if ($this->isConstantArray()->yes() && $typeToRemove instanceof HasOffsetValueType) { - return $this->unsetOffset($typeToRemove->getOffsetType()); - } - return null; } - public function exponentiate(Type $exponent): Type - { - return new ErrorType(); - } - public function getFiniteTypes(): array { return []; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self( - $properties['keyType'], - $properties['itemType'], - ); - } - } diff --git a/src/Type/BenevolentUnionType.php b/src/Type/BenevolentUnionType.php index dc1245ad45..6a2d28dc1c 100644 --- a/src/Type/BenevolentUnionType.php +++ b/src/Type/BenevolentUnionType.php @@ -14,9 +14,19 @@ class BenevolentUnionType extends UnionType * @api * @param Type[] $types */ - public function __construct(array $types) + public function __construct(array $types, bool $normalized = false) { - parent::__construct($types); + parent::__construct($types, $normalized); + } + + public function filterTypes(callable $filterCb): Type + { + $result = parent::filterTypes($filterCb); + if (!$result instanceof self && $result instanceof UnionType) { + return TypeUtils::toBenevolentUnion($result); + } + + return $result; } public function describe(VerbosityLevel $level): string @@ -87,16 +97,11 @@ protected function unionResults(callable $getResult): TrinaryLogic return TrinaryLogic::createNo()->lazyOr($this->getTypes(), $getResult); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic - { - return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; - } - - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { $result = AcceptsResult::createNo(); foreach ($this->getTypes() as $innerType) { - $result = $result->or($acceptingType->acceptsWithReason($innerType, $strictTypes)); + $result = $result->or($acceptingType->accepts($innerType, $strictTypes)); } return $result; @@ -173,12 +178,4 @@ public function traverseSimultaneously(Type $right, callable $cb): Type return $this; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self($properties['types']); - } - } diff --git a/src/Type/BitwiseFlagHelper.php b/src/Type/BitwiseFlagHelper.php index 7d5e4c3db9..37e0a4a5cd 100644 --- a/src/Type/BitwiseFlagHelper.php +++ b/src/Type/BitwiseFlagHelper.php @@ -7,10 +7,12 @@ use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Name\FullyQualified; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ReflectionProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantIntegerType; +#[AutowiredService] final class BitwiseFlagHelper { diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index 72be662c95..bbde2f2aa8 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -46,6 +46,16 @@ 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'; @@ -91,8 +101,7 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], - [], - TrinaryLogic::createYes(), + isList: TrinaryLogic::createYes(), ); } @@ -101,6 +110,15 @@ public function toArrayKey(): Type return new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]); } + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this->toString(), $this); + } + + return $this; + } + public function isOffsetAccessLegal(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -163,12 +181,16 @@ public function toPhpDocNode(): TypeNode return new IdentifierTypeNode('bool'); } - /** - * @param mixed[] $properties - */ - 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 e51cf45631..ddb857fc99 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -2,6 +2,7 @@ namespace PHPStan\Type; +use Closure; use PHPStan\Analyser\OutOfClassScope; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\Tag\TemplateTag; @@ -18,10 +19,12 @@ 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; @@ -52,7 +55,7 @@ class CallableType implements CompoundType, CallableParametersAcceptor use NonRemoveableTypeTrait; use NonGeneralizableTypeTrait; - /** @var array */ + /** @var list */ private array $parameters; private Type $returnType; @@ -67,7 +70,7 @@ class CallableType implements CompoundType, CallableParametersAcceptor /** * @api - * @param array|null $parameters + * @param list|null $parameters * @param array $templateTags */ public function __construct( @@ -101,9 +104,6 @@ public function isPure(): TrinaryLogic return $this->isPure; } - /** - * @return string[] - */ public function getReferencedClasses(): array { $classes = []; @@ -129,32 +129,27 @@ public function getConstantStrings(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType && !$type instanceof self) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } - return $this->isSuperTypeOfInternal($type, true); + return $this->isSuperTypeOfInternal($type, true)->toAcceptsResult(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType && !$type instanceof self) { return $type->isSubTypeOf($this); } - return $this->isSuperTypeOfInternal($type, false)->result; + return $this->isSuperTypeOfInternal($type, false); } - private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): AcceptsResult + private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): IsSuperTypeOfResult { - $isCallable = new AcceptsResult($type->isCallable(), []); + $isCallable = new IsSuperTypeOfResult($type->isCallable(), []); if ($isCallable->no()) { return $isCallable; } @@ -171,14 +166,20 @@ private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): Accep $typePure = $typePure->and($variant->isPure()); } - return $isCallable->and(new AcceptsResult($typePure, [])); + 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; @@ -194,24 +195,19 @@ private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): Accep 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->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; - } - - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult - { - return new AcceptsResult($this->isSubTypeOf($acceptingType), []); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -294,6 +290,11 @@ public function getUsedVariables(): array return []; } + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function toNumber(): Type { return new ErrorType(); @@ -329,9 +330,14 @@ public function toArrayKey(): Type return new ErrorType(); } - public function isOffsetAccessLegal(): TrinaryLogic + public function toCoercedArgumentType(bool $strictTypes): Type { - return TrinaryLogic::createMaybe(); + 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 @@ -350,7 +356,7 @@ public function getCallSiteVarianceMap(): TemplateTypeVarianceMap } /** - * @return array + * @return list */ public function getParameters(): array { @@ -390,10 +396,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])) { @@ -586,7 +594,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } @@ -670,20 +688,4 @@ public function toPhpDocNode(): TypeNode ); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self( - (bool) $properties['isCommonCallable'] ? null : $properties['parameters'], - (bool) $properties['isCommonCallable'] ? null : $properties['returnType'], - $properties['variadic'], - $properties['templateTypeMap'], - $properties['resolvedTemplateTypeMap'], - $properties['templateTags'], - $properties['isPure'], - ); - } - } diff --git a/src/Type/CallableTypeHelper.php b/src/Type/CallableTypeHelper.php index eb58de74bb..4e99e94cc9 100644 --- a/src/Type/CallableTypeHelper.php +++ b/src/Type/CallableTypeHelper.php @@ -9,14 +9,14 @@ use function count; use function sprintf; -class CallableTypeHelper +final class CallableTypeHelper { public static function isParametersAcceptorSuperTypeOf( CallableParametersAcceptor $ours, CallableParametersAcceptor $theirs, bool $treatMixedAsAny, - ): AcceptsResult + ): IsSuperTypeOfResult { $theirParameters = $theirs->getParameters(); $ourParameters = $ours->getParameters(); @@ -40,7 +40,7 @@ public static function isParametersAcceptorSuperTypeOf( } } - $result = AcceptsResult::createYes(); + $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])) { @@ -48,7 +48,7 @@ public static function isParametersAcceptorSuperTypeOf( continue; } - $accepts = new AcceptsResult(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, @@ -62,7 +62,7 @@ public static function isParametersAcceptorSuperTypeOf( $ourParameterType = $ourParameter->getType(); if ($ourParameter->isOptional() && !$theirParameter->isOptional()) { - $accepts = new AcceptsResult(TrinaryLogic::createNo(), [ + $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, @@ -72,14 +72,15 @@ public static function isParametersAcceptorSuperTypeOf( } if ($treatMixedAsAny) { - $isSuperType = $theirParameter->getType()->acceptsWithReason($ourParameterType, true); + $isSuperType = $theirParameter->getType()->accepts($ourParameterType, true); + $isSuperType = new IsSuperTypeOfResult($isSuperType->result, $isSuperType->reasons); } else { - $isSuperType = new AcceptsResult($theirParameter->getType()->isSuperTypeOf($ourParameterType), []); + $isSuperType = $theirParameter->getType()->isSuperTypeOf($ourParameterType); } if ($isSuperType->maybe()) { $verbosity = VerbosityLevel::getRecommendedLevelByType($theirParameter->getType(), $ourParameterType); - $isSuperType = new AcceptsResult($isSuperType->result, array_merge($isSuperType->reasons, [ + $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), @@ -93,21 +94,22 @@ public static function isParametersAcceptorSuperTypeOf( } if (!$treatMixedAsAny && $theirParameterCount < $ourParameterCount) { - $result = $result->and(AcceptsResult::createMaybe()); + $result = $result->and(IsSuperTypeOfResult::createMaybe()); } $theirReturnType = $theirs->getReturnType(); if ($treatMixedAsAny) { - $isReturnTypeSuperType = $ours->getReturnType()->acceptsWithReason($theirReturnType, true); + $isReturnTypeSuperType = $ours->getReturnType()->accepts($theirReturnType, true); + $isReturnTypeSuperType = new IsSuperTypeOfResult($isReturnTypeSuperType->result, $isReturnTypeSuperType->reasons); } else { - $isReturnTypeSuperType = new AcceptsResult($ours->getReturnType()->isSuperTypeOf($theirReturnType), []); + $isReturnTypeSuperType = $ours->getReturnType()->isSuperTypeOf($theirReturnType); } $pure = $ours->isPure(); if ($pure->yes()) { - $result = $result->and(new AcceptsResult($theirs->isPure(), [])); + $result = $result->and(new IsSuperTypeOfResult($theirs->isPure(), [])); } elseif ($pure->no()) { - $result = $result->and(new AcceptsResult($theirs->isPure()->negate(), [])); + $result = $result->and(new IsSuperTypeOfResult($theirs->isPure()->negate(), [])); } return $result->and($isReturnTypeSuperType); diff --git a/src/Type/CircularTypeAliasDefinitionException.php b/src/Type/CircularTypeAliasDefinitionException.php index ad4bbfa25e..d0502cb9a1 100644 --- a/src/Type/CircularTypeAliasDefinitionException.php +++ b/src/Type/CircularTypeAliasDefinitionException.php @@ -4,7 +4,7 @@ use Exception; -class CircularTypeAliasDefinitionException extends Exception +final class CircularTypeAliasDefinitionException extends Exception { } diff --git a/src/Type/ClassStringType.php b/src/Type/ClassStringType.php index 802d17e162..c5dae4f195 100644 --- a/src/Type/ClassStringType.php +++ b/src/Type/ClassStringType.php @@ -21,27 +21,22 @@ public function describe(VerbosityLevel $level): string return 'class-string'; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } - return new AcceptsResult($type->isClassStringType(), []); + return new AcceptsResult($type->isClassString(), []); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return $type->isClassStringType(); + return new IsSuperTypeOfResult($type->isClassString(), []); } public function isString(): TrinaryLogic @@ -69,7 +64,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic { return TrinaryLogic::createYes(); } @@ -89,12 +94,4 @@ public function toPhpDocNode(): TypeNode return new IdentifierTypeNode('class-string'); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self(); - } - } diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index 93b9989102..292557f12a 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -16,17 +16,18 @@ 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\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\Php\DummyParameter; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; @@ -36,10 +37,10 @@ use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; -use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; use PHPStan\Type\Traits\NonRemoveableTypeTrait; @@ -53,14 +54,13 @@ class ClosureType implements TypeWithClassName, CallableParametersAcceptor { use NonArrayTypeTrait; - use NonGenericTypeTrait; use NonIterableTypeTrait; use UndecidedComparisonTypeTrait; use NonOffsetAccessibleTypeTrait; use NonRemoveableTypeTrait; use NonGeneralizableTypeTrait; - /** @var array */ + /** @var list */ private array $parameters; private Type $returnType; @@ -78,9 +78,11 @@ class ClosureType implements TypeWithClassName, CallableParametersAcceptor /** @var SimpleImpurePoint[] */ private array $impurePoints; + private TrinaryLogic $acceptsNamedArguments; + /** * @api - * @param array|null $parameters + * @param list|null $parameters * @param array $templateTags * @param SimpleThrowPoint[] $throwPoints * @param ?SimpleImpurePoint[] $impurePoints @@ -99,8 +101,14 @@ public function __construct( ?array $impurePoints = null, private array $invalidateExpressions = [], private array $usedVariables = [], + ?TrinaryLogic $acceptsNamedArguments = null, ) { + if ($acceptsNamedArguments === null) { + $acceptsNamedArguments = TrinaryLogic::createYes(); + } + $this->acceptsNamedArguments = $acceptsNamedArguments; + $this->parameters = $parameters ?? []; $this->returnType = $returnType ?? new MixedType(); $this->isCommonCallable = $parameters === null && $returnType === null; @@ -158,9 +166,6 @@ public function getAncestorWithClassName(string $className): ?TypeWithClassName return $this->objectType->getAncestorWithClassName($className); } - /** - * @return string[] - */ public function getReferencedClasses(): array { $classes = $this->objectType->getReferencedClasses(); @@ -181,48 +186,48 @@ public function getObjectClassReflections(): array return $this->objectType->getObjectClassReflections(); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } if (!$type instanceof ClosureType) { - return $this->objectType->acceptsWithReason($type, $strictTypes); + 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)->result; + return $this->isSuperTypeOfInternal($type, false); } - private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): AcceptsResult + private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): IsSuperTypeOfResult { if ($type instanceof self) { + $parameterTypes = array_map(static fn ($parameter) => $parameter->getType(), $this->getParameters()); + $variant = ParametersAcceptorSelector::selectFromTypes($parameterTypes, [$type], false); + if (!$variant instanceof CallableParametersAcceptor) { + return IsSuperTypeOfResult::createNo([]); + } return CallableTypeHelper::isParametersAcceptorSuperTypeOf( $this, - $type, + $variant, $treatMixedAsAny, ); } if ($type->getObjectClassNames() === [Closure::class]) { - return AcceptsResult::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - return new AcceptsResult($this->objectType->isSuperTypeOf($type), []); + return $this->objectType->isSuperTypeOf($type); } public function equals(Type $type): bool @@ -231,7 +236,8 @@ public function equals(Type $type): bool return false; } - return $this->describe(VerbosityLevel::precise()) === $type->describe(VerbosityLevel::precise()); + return $this->describe(VerbosityLevel::precise()) === $type->describe(VerbosityLevel::precise()) + && $this->isPure()->equals($type->isPure()); } public function describe(VerbosityLevel $level): string @@ -300,7 +306,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); } @@ -347,7 +353,7 @@ 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); } @@ -407,6 +413,11 @@ public function getUsedVariables(): array return $this->usedVariables; } + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->acceptsNamedArguments; + } + public function isCloneable(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -448,8 +459,7 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], - [], - TrinaryLogic::createYes(), + isList: TrinaryLogic::createYes(), ); } @@ -458,6 +468,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function toCoercedArgumentType(bool $strictTypes): Type + { + return TypeCombinator::union($this, new CallableType()); + } + public function getTemplateTypeMap(): TemplateTypeMap { return $this->templateTypeMap; @@ -474,7 +489,7 @@ public function getCallSiteVarianceMap(): TemplateTypeVarianceMap } /** - * @return array + * @return list */ public function getParameters(): array { @@ -514,10 +529,12 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap private function inferTemplateTypesOnParametersAcceptor(ParametersAcceptor $parametersAcceptor): TemplateTypeMap { - $typeMap = TemplateTypeMap::createEmpty(); + $parameterTypes = array_map(static fn ($parameter) => $parameter->getType(), $this->getParameters()); + $parametersAcceptor = ParametersAcceptorSelector::selectFromTypes($parameterTypes, [$parametersAcceptor], false); $args = $parametersAcceptor->getParameters(); $returnType = $parametersAcceptor->getReturnType(); + $typeMap = TemplateTypeMap::createEmpty(); foreach ($this->getParameters() as $i => $param) { $paramType = $param->getType(); if (isset($args[$i])) { @@ -534,6 +551,23 @@ 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) { @@ -562,6 +596,7 @@ public function traverse(callable $cb): Type $this->impurePoints, $this->invalidateExpressions, $this->usedVariables, + $this->acceptsNamedArguments, ); } @@ -611,6 +646,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type $this->impurePoints, $this->invalidateExpressions, $this->usedVariables, + $this->acceptsNamedArguments, ); } @@ -689,7 +725,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -763,24 +809,4 @@ public function toPhpDocNode(): TypeNode ); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self( - $properties['parameters'], - $properties['returnType'], - $properties['variadic'], - $properties['templateTypeMap'], - $properties['resolvedTemplateTypeMap'], - $properties['callSiteVarianceMap'], - $properties['templateTags'], - $properties['throwPoints'], - $properties['impurePoints'], - $properties['invalidateExpressions'], - $properties['usedVariables'], - ); - } - } diff --git a/src/Type/ClosureTypeFactory.php b/src/Type/ClosureTypeFactory.php index 07a395a5fe..4bb994908e 100644 --- a/src/Type/ClosureTypeFactory.php +++ b/src/Type/ClosureTypeFactory.php @@ -13,6 +13,8 @@ use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; use PHPStan\BetterReflection\SourceLocator\SourceStubber\ReflectionSourceStubber; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\ParameterReflection; @@ -23,14 +25,18 @@ use function count; use function str_replace; -/** @api */ -class ClosureTypeFactory +/** + * @api + */ +#[AutowiredService] +final class ClosureTypeFactory { public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ReflectionSourceStubber $reflectionSourceStubber, private Reflector $reflector, + #[AutowiredParameter(ref: '@currentPhpVersionPhpParser')] private Parser $parser, ) { @@ -80,7 +86,7 @@ public function isOptional(): bool public function getType(): Type { - return TypehintHelper::decideTypeFromReflection(ReflectionType::fromTypeOrNull($this->reflection->getType()), null, null, $this->reflection->isVariadic()); + return TypehintHelper::decideTypeFromReflection(ReflectionType::fromTypeOrNull($this->reflection->getType()), isVariadic: $this->reflection->isVariadic()); } public function passedByReference(): PassedByReference diff --git a/src/Type/CompoundType.php b/src/Type/CompoundType.php index ea9b2f4660..f66e10c091 100644 --- a/src/Type/CompoundType.php +++ b/src/Type/CompoundType.php @@ -2,20 +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 isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult; + public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic; - public function isGreaterThan(Type $otherType): 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 index aa5d8af0a6..f154fb5368 100644 --- a/src/Type/ConditionalType.php +++ b/src/Type/ConditionalType.php @@ -4,7 +4,6 @@ use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\LateResolvableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; @@ -61,7 +60,7 @@ public function isNegated(): bool return $this->negated; } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { return $this->if->isSuperTypeOf($type->if) @@ -188,20 +187,6 @@ public function toPhpDocNode(): TypeNode ); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self( - $properties['subject'], - $properties['target'], - $properties['if'], - $properties['else'], - $properties['negated'], - ); - } - private function getNormalizedIf(): Type { return $this->normalizedIf ??= TypeTraverser::map( diff --git a/src/Type/ConditionalTypeForParameter.php b/src/Type/ConditionalTypeForParameter.php index 57c2fe5d8d..0fd8cf4475 100644 --- a/src/Type/ConditionalTypeForParameter.php +++ b/src/Type/ConditionalTypeForParameter.php @@ -4,7 +4,6 @@ use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\LateResolvableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; @@ -75,7 +74,7 @@ public function toConditional(Type $subject): Type ); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { return $this->if->isSuperTypeOf($type->if) @@ -175,18 +174,4 @@ public function toPhpDocNode(): TypeNode ); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self( - $properties['parameterName'], - $properties['target'], - $properties['if'], - $properties['else'], - $properties['negated'], - ); - } - } diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 3853794348..5643e79655 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -4,7 +4,6 @@ use Nette\Utils\Strings; use PHPStan\Analyser\OutOfClassScope; -use PHPStan\DependencyInjection\BleedingEdgeToggle; use PHPStan\Internal\CombinationsHelper; use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; @@ -24,21 +23,27 @@ 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\ConstantScalarType; -use PHPStan\Type\ConstantType; use PHPStan\Type\ErrorType; use PHPStan\Type\GeneralizePrecision; 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\NullType; +use PHPStan\Type\Traits\ArrayTypeTrait; +use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; @@ -48,7 +53,6 @@ use function array_merge; use function array_pop; use function array_push; -use function array_reverse; use function array_slice; use function array_unique; use function array_values; @@ -56,8 +60,6 @@ use function count; use function implode; use function in_array; -use function is_bool; -use function is_int; use function is_string; use function min; use function pow; @@ -69,77 +71,118 @@ /** * @api */ -class ConstantArrayType extends ArrayType implements ConstantType +class ConstantArrayType implements Type { + use ArrayTypeTrait { + chunkArray as traitChunkArray; + } + use NonObjectTypeTrait; + use UndecidedComparisonTypeTrait; + private const DESCRIBE_LIMIT = 8; + private const CHUNK_FINITE_TYPES_LIMIT = 5; private TrinaryLogic $isList; /** @var self[]|null */ private ?array $allArrays = null; - /** @var non-empty-list */ - private array $nextAutoIndexes; + private ?Type $iterableKeyType = null; + + private ?Type $iterableValueType = null; /** * @api * @param array $keyTypes * @param array $valueTypes - * @param non-empty-list|int $nextAutoIndexes + * @param non-empty-list $nextAutoIndexes * @param int[] $optionalKeys */ public function __construct( private array $keyTypes, private array $valueTypes, - int|array $nextAutoIndexes = [0], + private array $nextAutoIndexes = [0], private array $optionalKeys = [], - bool|TrinaryLogic $isList = false, + ?TrinaryLogic $isList = null, ) { assert(count($keyTypes) === count($valueTypes)); - if (is_int($nextAutoIndexes)) { - $nextAutoIndexes = [$nextAutoIndexes]; + $keyTypesCount = count($this->keyTypes); + if ($keyTypesCount === 0) { + $isList = TrinaryLogic::createYes(); } - $this->nextAutoIndexes = $nextAutoIndexes; + 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); - $isList = TrinaryLogic::createYes(); } elseif ($keyTypesCount === 1) { $keyType = $this->keyTypes[0]; } else { $keyType = new UnionType($this->keyTypes); } - if (is_bool($isList)) { - $isList = TrinaryLogic::createFromBoolean($isList); + return $this->iterableKeyType = $keyType; + } + + public function getIterableValueType(): Type + { + if ($this->iterableValueType !== null) { + return $this->iterableValueType; } - $this->isList = $isList; - parent::__construct( - $keyType, - count($valueTypes) > 0 ? TypeCombinator::union(...$valueTypes) : new NeverType(true), - ); + return $this->iterableValueType = count($this->valueTypes) > 0 ? TypeCombinator::union(...$this->valueTypes) : new NeverType(true); } - public function getConstantArrays(): array + public function getKeyType(): Type { - return [$this]; + return $this->getIterableKeyType(); } - public function isConstantValue(): TrinaryLogic + public function getItemType(): Type { - return TrinaryLogic::createYes(); + return $this->getIterableValueType(); } - /** @deprecated Use isIterableAtLeastOnce()->no() instead */ - public function isEmpty(): bool + public function isConstantValue(): TrinaryLogic { - return count($this->keyTypes) === 0; + return TrinaryLogic::createYes(); } /** @@ -150,14 +193,6 @@ public function getNextAutoIndexes(): array return $this->nextAutoIndexes; } - /** - * @deprecated - */ - public function getNextAutoIndex(): int - { - return $this->nextAutoIndexes[count($this->nextAutoIndexes) - 1]; - } - /** * @return int[] */ @@ -207,7 +242,7 @@ public function getAllArrays(): array } $array = $builder->getArray(); - if (!$array instanceof ConstantArrayType) { + if (!$array instanceof self) { throw new ShouldNotHappenException(); } @@ -251,18 +286,6 @@ public function getKeyTypes(): array return $this->keyTypes; } - /** @deprecated Use getFirstIterableKeyType() instead */ - public function getFirstKeyType(): Type - { - return $this->getFirstIterableKeyType(); - } - - /** @deprecated Use getLastIterableKeyType() instead */ - public function getLastKeyType(): Type - { - return $this->getLastIterableKeyType(); - } - /** * @return array */ @@ -271,32 +294,15 @@ public function getValueTypes(): array return $this->valueTypes; } - /** @deprecated Use getFirstIterableValueType() instead */ - public function getFirstValueType(): Type - { - return $this->getFirstIterableValueType(); - } - - /** @deprecated Use getLastIterableValueType() instead */ - public function getLastValueType(): Type - { - return $this->getLastIterableValueType(); - } - public function isOptionalKey(int $i): bool { return in_array($i, $this->optionalKeys, true); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType && !$type instanceof IntersectionType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } if ($type instanceof self && count($this->keyTypes) === 0) { @@ -324,10 +330,10 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult $result = $result->and($hasOffset); $otherValueType = $type->getOffsetValueType($keyType); $verbosity = VerbosityLevel::getRecommendedLevelByType($valueType, $otherValueType); - $acceptsValue = $valueType->acceptsWithReason($otherValueType, $strictTypes)->decorateReasons( + $acceptsValue = $valueType->accepts($otherValueType, $strictTypes)->decorateReasons( static fn (string $reason) => sprintf( 'Offset %s (%s) does not accept type %s: %s', - $keyType->describe(VerbosityLevel::value()), + $keyType->describe(VerbosityLevel::precise()), $valueType->describe($verbosity), $otherValueType->describe($verbosity), $reason, @@ -337,7 +343,7 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult $acceptsValue = new AcceptsResult($acceptsValue->result, [ sprintf( 'Offset %s (%s) does not accept type %s.', - $keyType->describe(VerbosityLevel::value()), + $keyType->describe(VerbosityLevel::precise()), $valueType->describe($verbosity), $otherValueType->describe($verbosity), ), @@ -359,11 +365,11 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult return $result; } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { if (count($this->keyTypes) === 0) { - return $type->isIterableAtLeastOnce()->negate(); + return new IsSuperTypeOfResult($type->isIterableAtLeastOnce()->negate(), []); } $results = []; @@ -371,34 +377,34 @@ 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::createYes(); + $results[] = IsSuperTypeOfResult::createYes(); continue; } elseif ($hasOffset->maybe() && !$this->isOptionalKey($i)) { - $results[] = TrinaryLogic::createMaybe(); + $results[] = IsSuperTypeOfResult::createMaybe(); } $isValueSuperType = $this->valueTypes[$i]->isSuperTypeOf($type->getOffsetValueType($keyType)); if ($isValueSuperType->no()) { - return TrinaryLogic::createNo(); + return $isValueSuperType->decorateReasons(static fn (string $reason) => sprintf('Offset %s: %s', $keyType->describe(VerbosityLevel::value()), $reason)); } $results[] = $isValueSuperType; } - return TrinaryLogic::createYes()->and(...$results); + return IsSuperTypeOfResult::createYes()->and(...$results); } if ($type instanceof ArrayType) { - $result = TrinaryLogic::createMaybe(); + $result = IsSuperTypeOfResult::createMaybe(); if (count($this->keyTypes) === 0) { return $result; } $isKeySuperType = $this->getKeyType()->isSuperTypeOf($type->getKeyType()); if ($isKeySuperType->no()) { - return TrinaryLogic::createNo(); + return $isKeySuperType; } return $result->and($isKeySuperType, $this->getItemType()->isSuperTypeOf($type->getItemType())); @@ -408,14 +414,30 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { - if ($this->isIterableAtLeastOnce()->no() && count($type->getConstantScalarValues()) === 1) { - // @phpstan-ignore equal.invalid, equal.notAllowed - return new ConstantBooleanType($type->getConstantScalarValues()[0] == []); // phpcs:ignore + 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(); @@ -491,10 +513,8 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) return $acceptors; } - /** - * @return array{Type, Type}|array{} - */ - private function getClassOrObjectAndMethods(): array + /** @return ConstantArrayTypeAndMethod[] */ + public function findTypeAndMethodNames(): array { if (count($this->keyTypes) !== 2) { return []; @@ -519,46 +539,7 @@ private function getClassOrObjectAndMethods(): array return []; } - return [$classOrObject, $method]; - } - - /** @deprecated Use findTypeAndMethodNames() instead */ - public function findTypeAndMethodName(): ?ConstantArrayTypeAndMethod - { - $callableArray = $this->getClassOrObjectAndMethods(); - if ($callableArray === []) { - return null; - } - - [$classOrObject, $method] = $callableArray; - if (!$method instanceof ConstantStringType) { - return ConstantArrayTypeAndMethod::createUnknown(); - } - - $type = $classOrObject->getObjectTypeOrClassStringObjectType(); - if (!$type->isObject()->yes()) { - return ConstantArrayTypeAndMethod::createUnknown(); - } - - $has = $type->hasMethod($method->getValue()); - if (!$has->no()) { - if ($this->isOptionalKey(0) || $this->isOptionalKey(1)) { - $has = $has->and(TrinaryLogic::createMaybe()); - } - - return ConstantArrayTypeAndMethod::createConcrete($type, $method->getValue(), $has); - } - - return null; - } - - /** @return ConstantArrayTypeAndMethod[] */ - public function findTypeAndMethodNames(): array - { - $callableArray = $this->getClassOrObjectAndMethods(); - if ($callableArray === []) { - return []; - } + $callableArray = [$classOrObject, $method]; [$classOrObject, $methods] = $callableArray; if (count($methods->getConstantStrings()) === 0) { @@ -572,18 +553,17 @@ public function findTypeAndMethodNames(): array $typeAndMethods = []; $phpVersion = PhpVersionStaticAccessor::getInstance(); - foreach ($methods->getConstantStrings() as $method) { - $has = $type->hasMethod($method->getValue()); + foreach ($methods->getConstantStrings() as $methodName) { + $has = $type->hasMethod($methodName->getValue()); if ($has->no()) { continue; } if ( - BleedingEdgeToggle::isBleedingEdge() - && $has->yes() + $has->yes() && !$phpVersion->supportsCallableInstanceMethods() ) { - $methodReflection = $type->getMethod($method->getValue(), new OutOfClassScope()); + $methodReflection = $type->getMethod($methodName->getValue(), new OutOfClassScope()); if ($classOrObject->isString()->yes() && !$methodReflection->isStatic()) { continue; } @@ -593,7 +573,7 @@ public function findTypeAndMethodNames(): array $has = $has->and(TrinaryLogic::createMaybe()); } - $typeAndMethods[] = ConstantArrayTypeAndMethod::createConcrete($type, $method->getValue(), $has); + $typeAndMethods[] = ConstantArrayTypeAndMethod::createConcrete($type, $methodName->getValue(), $has); } return $typeAndMethods; @@ -601,11 +581,17 @@ public function findTypeAndMethodNames(): array public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - $offsetType = $offsetType->toArrayKey(); + $offsetArrayKeyType = $offsetType->toArrayKey(); + + return $this->recursiveHasOffsetValueType($offsetArrayKeyType); + } + + private function recursiveHasOffsetValueType(Type $offsetType): TrinaryLogic + { if ($offsetType instanceof UnionType) { $results = []; foreach ($offsetType->getTypes() as $innerType) { - $results[] = $this->hasOffsetValueType($innerType); + $results[] = $this->recursiveHasOffsetValueType($innerType); } return TrinaryLogic::extremeIdentity(...$results); @@ -615,7 +601,7 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic if ($finiteTypes !== []) { $results = []; foreach ($finiteTypes as $innerType) { - $results[] = $this->hasOffsetValueType($innerType); + $results[] = $this->recursiveHasOffsetValueType($innerType); } return TrinaryLogic::extremeIdentity(...$results); @@ -651,12 +637,26 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic public function getOffsetValueType(Type $offsetType): Type { + 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; } @@ -664,10 +664,6 @@ public function getOffsetValueType(Type $offsetType): Type } if ($all) { - if (count($this->keyTypes) === 0) { - return new ErrorType(); - } - return $this->getIterableValueType(); } @@ -680,6 +676,10 @@ public function getOffsetValueType(Type $offsetType): Type return $type; } + if ($maybeAll) { + return $this->getIterableValueType(); + } + return new ErrorType(); // undefined offset } @@ -693,15 +693,8 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - $offsetType = $offsetType->toArrayKey(); $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); - foreach ($this->keyTypes as $keyType) { - if ($offsetType->isSuperTypeOf($keyType)->no()) { - continue; - } - - $builder->setOffsetValueType($keyType, $valueType); - } + $builder->setOffsetValueType($offsetType, $valueType); return $builder->getArray(); } @@ -780,6 +773,36 @@ public function unsetOffset(Type $offsetType): Type return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, $isList); } + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + $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); + } + + $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 $this->traitChunkArray($lengthType, $preserveKeys); + } + public function fillKeysArray(Type $valueType): Type { $builder = ConstantArrayTypeBuilder::createEmpty(); @@ -837,22 +860,46 @@ public function popArray(): Type return $this->removeLastElements(1); } - public function searchArray(Type $needleType): Type + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) { + $offsetType = $preserveKeys->yes() || $this->keyTypes[$i]->isInteger()->no() + ? $this->keyTypes[$i] + : null; + $builder->setOffsetValueType($offsetType, $this->valueTypes[$i], $this->isOptionalKey($i)); + } + + return $builder->getArray(); + } + + public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type { + $strict ??= TrinaryLogic::createMaybe(); $matches = []; $hasIdenticalValue = false; foreach ($this->valueTypes as $index => $valueType) { - $isNeedleSuperType = $valueType->isSuperTypeOf($needleType); - if ($isNeedleSuperType->no()) { - continue; + if ($strict->yes()) { + $isNeedleSuperType = $valueType->isSuperTypeOf($needleType); + if ($isNeedleSuperType->no()) { + continue; + } } - if ($needleType instanceof ConstantScalarType && $valueType instanceof ConstantScalarType - && $needleType->getValue() === $valueType->getValue() - && !$this->isOptionalKey($index) - ) { - $hasIdenticalValue = true; + 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; + } } $matches[] = $this->keyTypes[$index]; @@ -876,23 +923,212 @@ public function shiftArray(): Type public function shuffleArray(): Type { - $valuesArray = $this->getValuesArray(); + return $this->getValuesArray()->degradeToGeneralArray(); + } - $isIterableAtLeastOnce = $valuesArray->isIterableAtLeastOnce(); - if ($isIterableAtLeastOnce->no()) { - return $valuesArray; + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + $keyTypesCount = count($this->keyTypes); + if ($keyTypesCount === 0) { + return $this; } - $generalizedArray = new ArrayType($valuesArray->getIterableKeyType(), $valuesArray->getItemType()); + $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : null; - if ($isIterableAtLeastOnce->yes()) { - $generalizedArray = TypeCombinator::intersect($generalizedArray, new NonEmptyArrayType()); + if ($lengthType instanceof ConstantIntegerType) { + $length = $lengthType->getValue(); + } elseif ($lengthType->isNull()->yes()) { + $length = $keyTypesCount; + } else { + $length = null; } - if ($valuesArray->isList->yes()) { - $generalizedArray = AccessoryArrayListType::intersectWith($generalizedArray); + + if ($offset === null || $length === null) { + return $this->degradeToGeneralArray() + ->sliceArray($offsetType, $lengthType, $preserveKeys); } - return $generalizedArray; + if ($keyTypesCount + $offset <= 0) { + // A negative offset cannot reach left outside the array twice + $offset = 0; + } + + if ($keyTypesCount + $length <= 0) { + // A negative length cannot reach left outside the array twice + $length = 0; + } + + if ($length === 0 || ($offset < 0 && $length < 0 && $offset - $length >= 0)) { + // 0 / 0, 3 / 0 or e.g. -3 / -3 or -3 / -4 and so on never extract anything + return new self([], []); + } + + if ($length < 0) { + // Negative lengths prevent access to the most right n elements + return $this->removeLastElements($length * -1) + ->sliceArray($offsetType, new NullType(), $preserveKeys); + } + + if ($offset < 0) { + /* + * Transforms the problem with the negative offset in one with a positive offset using array reversion. + * The reason is belows handling of optional keys which works only from left to right. + * + * e.g. + * array{a: 0, b: 1, c: 2, d: 3, e: 4} + * with offset -4 and length 2 (which would be sliced to array{b: 1, c: 2}) + * + * is transformed via reversion to + * + * array{e: 4, d: 3, c: 2, b: 1, a: 0} + * with offset 2 and length 2 (which will be sliced to array{c: 2, b: 1} and then reversed again) + */ + $offset *= -1; + $reversedLength = min($length, $offset); + $reversedOffset = $offset - $reversedLength; + return $this->reverseArray(TrinaryLogic::createYes()) + ->sliceArray(new ConstantIntegerType($reversedOffset), new ConstantIntegerType($reversedLength), $preserveKeys) + ->reverseArray(TrinaryLogic::createYes()); + } + + if ($offset > 0) { + return $this->removeFirstElements($offset, false) + ->sliceArray(new ConstantIntegerType(0), $lengthType, $preserveKeys); + } + + $builder = ConstantArrayTypeBuilder::createEmpty(); + + $nonOptionalElementsCount = 0; + $hasOptional = false; + for ($i = 0; $nonOptionalElementsCount < $length && $i < $keyTypesCount; $i++) { + $isOptional = $this->isOptionalKey($i); + if (!$isOptional) { + $nonOptionalElementsCount++; + } else { + $hasOptional = true; + } + + $isLastElement = $nonOptionalElementsCount >= $length || $i + 1 >= $keyTypesCount; + if ($isLastElement && $length < $keyTypesCount && $hasOptional) { + // If the slice is not full yet, but has at least one optional key + // the last non-optional element is going to be optional. + // Otherwise, it would not fit into the slice if previous non-optional keys are there. + $isOptional = true; + } + + $offsetType = $preserveKeys->yes() || $this->keyTypes[$i]->isInteger()->no() + ? $this->keyTypes[$i] + : null; + + $builder->setOffsetValueType($offsetType, $this->valueTypes[$i], $isOptional); + } + + return $builder->getArray(); + } + + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + $keyTypesCount = count($this->keyTypes); + if ($keyTypesCount === 0) { + return $this; + } + + $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : null; + + if ($lengthType instanceof ConstantIntegerType) { + $length = $lengthType->getValue(); + } elseif ($lengthType->isNull()->yes()) { + $length = $keyTypesCount; + } else { + $length = null; + } + + if ($offset === null || $length === null) { + 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, + ); + } + + $types[] = $builder->getArray(); + } + + return TypeCombinator::union(...$types); } public function isIterableAtLeastOnce(): TrinaryLogic @@ -983,12 +1219,6 @@ public function isList(): TrinaryLogic return $this->isList; } - /** @deprecated Use popArray() instead */ - public function removeLast(): self - { - return $this->removeLastElements(1); - } - /** @param positive-int $length */ private function removeLastElements(int $length): self { @@ -1000,7 +1230,7 @@ private function removeLastElements(int $length): self $keyTypes = $this->keyTypes; $valueTypes = $this->valueTypes; $optionalKeys = $this->optionalKeys; - $nextAutoindex = $this->nextAutoIndexes; + $nextAutoindexes = $this->nextAutoIndexes; $optionalKeysRemoved = 0; $newLength = $keyTypesCount - $length; @@ -1020,9 +1250,9 @@ private function removeLastElements(int $length): self $removedKeyType = array_pop($keyTypes); array_pop($valueTypes); - $nextAutoindex = $removedKeyType instanceof ConstantIntegerType - ? $removedKeyType->getValue() - : $this->getNextAutoIndex(); // @phpstan-ignore method.deprecated + $nextAutoindexes = $removedKeyType instanceof ConstantIntegerType + ? [$removedKeyType->getValue()] + : $this->nextAutoIndexes; continue; } @@ -1037,20 +1267,14 @@ private function removeLastElements(int $length): self return new self( $keyTypes, $valueTypes, - $nextAutoindex, + $nextAutoindexes, array_values($optionalKeys), $this->isList, ); } - /** @deprecated Use shiftArray() instead */ - public function removeFirst(): self - { - return $this->removeFirstElements(1); - } - /** @param positive-int $length */ - private function removeFirstElements(int $length, bool $reindex = true): self + private function removeFirstElements(int $length, bool $reindex = true): Type { $builder = ConstantArrayTypeBuilder::createEmpty(); @@ -1077,138 +1301,7 @@ private function removeFirstElements(int $length, bool $reindex = true): self $builder->setOffsetValueType($keyType, $valueType, $isOptional); } - $array = $builder->getArray(); - if (!$array instanceof self) { - throw new ShouldNotHappenException(); - } - - return $array; - } - - public function slice(int $offset, ?int $limit, bool $preserveKeys = false): self - { - $keyTypesCount = count($this->keyTypes); - if ($keyTypesCount === 0) { - return $this; - } - - $limit ??= $keyTypesCount; - if ($limit < 0) { - // Negative limits prevent access to the most right n elements - return $this->removeLastElements($limit * -1) - ->slice($offset, null, $preserveKeys); - } - - if ($keyTypesCount + $offset <= 0) { - // A negative offset cannot reach left outside the array - $offset = 0; - } - - 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 limit 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 limit 2 (which will be sliced to array{c: 2, b: 1} and then reversed again) - */ - $offset *= -1; - $reversedLimit = min($limit, $offset); - $reversedOffset = $offset - $reversedLimit; - return $this->reverse(true) - ->slice($reversedOffset, $reversedLimit, $preserveKeys) - ->reverse(true); - } - - if ($offset > 0) { - return $this->removeFirstElements($offset, false) - ->slice(0, $limit, $preserveKeys); - } - - $builder = ConstantArrayTypeBuilder::createEmpty(); - - $nonOptionalElementsCount = 0; - $hasOptional = false; - for ($i = 0; $nonOptionalElementsCount < $limit && $i < $keyTypesCount; $i++) { - $isOptional = $this->isOptionalKey($i); - if (!$isOptional) { - $nonOptionalElementsCount++; - } else { - $hasOptional = true; - } - - $isLastElement = $nonOptionalElementsCount >= $limit || $i + 1 >= $keyTypesCount; - if ($isLastElement && $limit < $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; - } - - $builder->setOffsetValueType($this->keyTypes[$i], $this->valueTypes[$i], $isOptional); - } - - $slice = $builder->getArray(); - if (!$slice instanceof self) { - throw new ShouldNotHappenException(); - } - - return $preserveKeys ? $slice : $slice->reindex(); - } - - public function reverse(bool $preserveKeys = false): self - { - $keyTypesReversed = array_reverse($this->keyTypes, true); - $keyTypes = array_values($keyTypesReversed); - $keyTypesReversedKeys = array_keys($keyTypesReversed); - $optionalKeys = array_map(static fn (int $optionalKey): int => $keyTypesReversedKeys[$optionalKey], $this->optionalKeys); - - $reversed = new self($keyTypes, array_reverse($this->valueTypes), $this->nextAutoIndexes, $optionalKeys, TrinaryLogic::createNo()); - - return $preserveKeys ? $reversed : $reversed->reindex(); - } - - /** @param positive-int $length */ - public function chunk(int $length, bool $preserveKeys = false): self - { - $builder = ConstantArrayTypeBuilder::createEmpty(); - - $keyTypesCount = count($this->keyTypes); - for ($i = 0; $i < $keyTypesCount; $i += $length) { - $chunk = $this->slice($i, $length, true); - $builder->setOffsetValueType(null, $preserveKeys ? $chunk : $chunk->getValuesArray()); - } - - $chunks = $builder->getArray(); - if (!$chunks instanceof self) { - throw new ShouldNotHappenException(); - } - - return $chunks; - } - - private function reindex(): self - { - $keyTypes = []; - $autoIndex = 0; - - foreach ($this->keyTypes as $keyType) { - if (!$keyType instanceof ConstantIntegerType) { - $keyTypes[] = $keyType; - continue; - } - - $keyTypes[] = new ConstantIntegerType($autoIndex); - $autoIndex++; - } - - return new self($keyTypes, $this->valueTypes, [$autoIndex], $this->optionalKeys, TrinaryLogic::createYes()); + return $builder->getArray(); } public function toBoolean(): BooleanType @@ -1238,7 +1331,7 @@ public function generalize(GeneralizePrecision $precision): Type $arrayType = new ArrayType( $this->getIterableKeyType()->generalize($precision), - $this->getItemType()->generalize($precision), + $this->getIterableValueType()->generalize($precision), ); $keyTypesCount = count($this->keyTypes); @@ -1258,7 +1351,7 @@ public function generalize(GeneralizePrecision $precision): Type } if ($this->isList()->yes()) { - $arrayType = AccessoryArrayListType::intersectWith($arrayType); + $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType()); } if (count($accessoryTypes) > 0) { @@ -1268,10 +1361,7 @@ public function generalize(GeneralizePrecision $precision): Type return $arrayType; } - /** - * @return self - */ - public function generalizeValues(): ArrayType + public function generalizeValues(): self { $valueTypes = []; foreach ($this->valueTypes as $valueType) { @@ -1281,38 +1371,33 @@ public function generalizeValues(): ArrayType return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); } - /** @deprecated */ - public function generalizeToArray(): Type + private function degradeToGeneralArray(): Type { - $isIterableAtLeastOnce = $this->isIterableAtLeastOnce(); - if ($isIterableAtLeastOnce->no()) { - return $this; - } + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); + $builder->degradeToGeneralArray(); - $arrayType = new ArrayType($this->getIterableKeyType(), $this->getItemType()); + return $builder->getArray(); + } - if ($isIterableAtLeastOnce->yes()) { - $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); - } - if ($this->isList->yes()) { - $arrayType = AccessoryArrayListType::intersectWith($arrayType); - } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + $keysArray = $this->getKeysOrValuesArray($this->keyTypes); - return $arrayType; + return TypeCombinator::intersect( + new ArrayType( + new IntegerType(), + $keysArray->getIterableValueType(), + ), + new AccessoryArrayListType(), + ); } - /** - * @return self - */ - public function getKeysArray(): Type + public function getKeysArray(): self { return $this->getKeysOrValuesArray($this->keyTypes); } - /** - * @return self - */ - public function getValuesArray(): Type + public function getValuesArray(): self { return $this->getKeysOrValuesArray($this->valueTypes); } @@ -1364,12 +1449,6 @@ private function getKeysOrValuesArray(array $types): self return new self($keyTypes, $valueTypes, $autoIndexes, $optionalKeys, TrinaryLogic::createYes()); } - /** @deprecated Use getArraySize() instead */ - public function count(): Type - { - return $this->getArraySize(); - } - public function describe(VerbosityLevel $level): string { $describeValue = function (bool $truncate) use ($level): string { @@ -1393,6 +1472,8 @@ public function describe(VerbosityLevel $level): string $keyDescription = sprintf('\'%s\'', $keyDescription); } elseif (str_contains($keyDescription, '\'')) { $keyDescription = sprintf('"%s"', $keyDescription); + } elseif (!self::isValidIdentifier($keyDescription)) { + $keyDescription = sprintf('\'%s\'', $keyDescription); } } @@ -1415,7 +1496,7 @@ public function describe(VerbosityLevel $level): string ); }; return $level->handle( - fn (): string => parent::describe($level), + fn (): string => $this->isIterableAtLeastOnce()->no() ? 'array' : sprintf('array<%s, %s>', $this->getIterableKeyType()->describe($level), $this->getIterableValueType()->describe($level)), static fn (): string => $describeValue(true), static fn (): string => $describeValue(false), ); @@ -1441,7 +1522,14 @@ 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 @@ -1464,6 +1552,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 = []; @@ -1674,7 +1783,7 @@ public function toPhpDocNode(): TypeNode ); } - return new ArrayShapeNode($exportValuesOnly ? $values : $items); + return ArrayShapeNode::createSealed($exportValuesOnly ? $values : $items); } public static function isValidIdentifier(string $value): bool @@ -1723,12 +1832,4 @@ public function getFiniteTypes(): array return $finiteTypes; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self($properties['keyTypes'], $properties['valueTypes'], $properties['nextAutoIndexes'] ?? $properties['nextAutoIndex'], $properties['optionalKeys'] ?? [], $properties['isList'] ?? TrinaryLogic::createNo()); - } - } diff --git a/src/Type/Constant/ConstantArrayTypeAndMethod.php b/src/Type/Constant/ConstantArrayTypeAndMethod.php index e5bd1caf7b..07f4156550 100644 --- a/src/Type/Constant/ConstantArrayTypeAndMethod.php +++ b/src/Type/Constant/ConstantArrayTypeAndMethod.php @@ -6,8 +6,10 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -/** @api */ -class ConstantArrayTypeAndMethod +/** + * @api + */ +final class ConstantArrayTypeAndMethod { private function __construct( diff --git a/src/Type/Constant/ConstantArrayTypeBuilder.php b/src/Type/Constant/ConstantArrayTypeBuilder.php index 8547fa39a9..a639bf6c0e 100644 --- a/src/Type/Constant/ConstantArrayTypeBuilder.php +++ b/src/Type/Constant/ConstantArrayTypeBuilder.php @@ -22,8 +22,10 @@ use function min; use function range; -/** @api */ -class ConstantArrayTypeBuilder +/** + * @api + */ +final class ConstantArrayTypeBuilder { public const ARRAY_COUNT_LIMIT = 256; @@ -125,6 +127,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt if (count($this->keyTypes) > self::ARRAY_COUNT_LIMIT) { $this->degradeToGeneralArray = true; + $this->oversized = true; } return; @@ -162,16 +165,22 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt if ($offsetType instanceof ConstantIntegerType) { $min = min($this->nextAutoIndexes); $max = max($this->nextAutoIndexes); - if ($offsetType->getValue() > $min) { - if ($offsetType->getValue() <= $max) { - $this->isList = $this->isList->and(TrinaryLogic::createMaybe()); - } else { - $this->isList = TrinaryLogic::createNo(); + $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 ($offsetType->getValue() >= $max) { + + if ($offsetValue >= $max) { /** @var int|float $newAutoIndex */ - $newAutoIndex = $offsetType->getValue() + 1; + $newAutoIndex = $offsetValue + 1; if (is_float($newAutoIndex)) { $newAutoIndex = $max; } @@ -191,13 +200,12 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt if (count($this->keyTypes) > self::ARRAY_COUNT_LIMIT) { $this->degradeToGeneralArray = true; + $this->oversized = true; } return; } - $this->isList = TrinaryLogic::createNo(); - $scalarTypes = $offsetType->getConstantScalarTypes(); if (count($scalarTypes) === 0) { $integerRanges = TypeUtils::getIntegerRanges($offsetType); @@ -254,6 +262,8 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt return; } } + + $this->isList = TrinaryLogic::createNo(); } if ($offsetType === null) { @@ -303,7 +313,7 @@ public function getArray(): Type } if ($this->isList->yes()) { - $array = AccessoryArrayListType::intersectWith($array); + $array = TypeCombinator::intersect($array, new AccessoryArrayListType()); } return $array; diff --git a/src/Type/Constant/ConstantBooleanType.php b/src/Type/Constant/ConstantBooleanType.php index db5328d0d2..282b005c15 100644 --- a/src/Type/Constant/ConstantBooleanType.php +++ b/src/Type/Constant/ConstantBooleanType.php @@ -14,6 +14,7 @@ use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; /** @api */ @@ -40,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(); @@ -48,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(); @@ -56,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(); @@ -64,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(); @@ -107,6 +108,15 @@ 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 TrinaryLogic::createFromBoolean($this->value === true); @@ -127,14 +137,6 @@ public function toPhpDocNode(): TypeNode return new IdentifierTypeNode($this->value ? 'true' : 'false'); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self($properties['value']); - } - public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { if ($type->isObject()->yes()) { diff --git a/src/Type/Constant/ConstantFloatType.php b/src/Type/Constant/ConstantFloatType.php index 3aeb1e2e18..0ac763af76 100644 --- a/src/Type/Constant/ConstantFloatType.php +++ b/src/Type/Constant/ConstantFloatType.php @@ -100,12 +100,4 @@ public function toPhpDocNode(): TypeNode return new ConstTypeNode(new ConstExprFloatNode($this->castFloatToString($this->value))); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self($properties['value']); - } - } diff --git a/src/Type/Constant/ConstantIntegerType.php b/src/Type/Constant/ConstantIntegerType.php index 9226acacc2..6b482c62e6 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -5,15 +5,16 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -use PHPStan\TrinaryLogic; use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; 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; @@ -37,31 +38,31 @@ public function getValue(): int return $this->value; } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return $this->value === $type->value ? TrinaryLogic::createYes() : TrinaryLogic::createNo(); + return $this->value === $type->value ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createNo(); } if ($type instanceof IntegerRangeType) { $min = $type->getMin(); $max = $type->getMax(); if (($min === null || $min <= $this->value) && ($max === null || $this->value <= $max)) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function describe(VerbosityLevel $level): string @@ -92,6 +93,15 @@ 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(); @@ -105,12 +115,4 @@ public function toPhpDocNode(): TypeNode return new ConstTypeNode(new ConstExprIntegerNode((string) $this->value)); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self($properties['value']); - } - } diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index fbdbaf2f97..e4c34e609f 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -6,13 +6,13 @@ use Nette\Utils\Strings; use PhpParser\Node\Name; use PHPStan\Analyser\OutOfClassScope; -use PHPStan\DependencyInjection\BleedingEdgeToggle; -use PHPStan\PhpDocParser\Ast\ConstExpr\QuoteAwareConstExprStringNode; +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\ConstantReflection; use PHPStan\Reflection\InaccessibleMethod; use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\ReflectionProviderStaticAccessor; @@ -20,9 +20,11 @@ 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; @@ -32,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; @@ -48,6 +51,8 @@ use function is_numeric; use function key; use function strlen; +use function strtolower; +use function strtoupper; use function substr; use function substr_count; @@ -80,7 +85,7 @@ public function getConstantStrings(): array return [$this]; } - public function isClassStringType(): TrinaryLogic + public function isClassString(): TrinaryLogic { if ($this->isClassString) { return TrinaryLogic::createYes(); @@ -93,7 +98,7 @@ public function isClassStringType(): TrinaryLogic public function getClassStringObjectType(): Type { - if ($this->isClassStringType()->yes()) { + if ($this->isClassString()->yes()) { return new ObjectType($this->value); } @@ -105,14 +110,6 @@ public function getObjectTypeOrClassStringObjectType(): Type return $this->getClassStringObjectType(); } - /** - * @deprecated use isClassStringType() instead - */ - public function isClassString(): bool - { - return $this->isClassStringType()->yes(); - } - public function describe(VerbosityLevel $level): string { return $level->handle( @@ -144,12 +141,12 @@ private function export(string $value): string return "'" . addcslashes($value, '\\\'') . "'"; } - public function isSuperTypeOf(Type $type): TrinaryLogic + 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(); @@ -169,27 +166,27 @@ public function isSuperTypeOf(Type $type): TrinaryLogic // Explicitly handle the uncertainty for Yes & Maybe. if ($isSuperType->yes()) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } if ($type instanceof ClassStringType) { - return $this->isClassStringType()->yes() ? 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 @@ -217,8 +214,7 @@ public function isCallable(): TrinaryLogic if ($classRef->hasMethod($matches[2])) { $method = $classRef->getMethod($matches[2], new OutOfClassScope()); if ( - BleedingEdgeToggle::isBleedingEdge() - && !$phpVersion->supportsCallableInstanceMethods() + !$phpVersion->supportsCallableInstanceMethods() && !$method->isStatic() ) { return TrinaryLogic::createNo(); @@ -227,7 +223,7 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createYes(); } - if (!$classRef->getNativeReflection()->isFinal()) { + if (!$classRef->isFinalByKeyword()) { return TrinaryLogic::createMaybe(); } @@ -269,7 +265,7 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) return FunctionCallableVariant::createFromVariants($method, $method->getVariants()); } - if (!$classReflection->getNativeReflection()->isFinal()) { + if (!$classReflection->isFinalByKeyword()) { return [new TrivialParametersAcceptor()]; } } @@ -343,12 +339,22 @@ 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); @@ -356,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); @@ -430,6 +459,14 @@ public function generalize(GeneralizePrecision $precision): Type $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); } @@ -443,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), @@ -462,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), @@ -475,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), @@ -489,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), @@ -504,7 +541,7 @@ public function getGreaterOrEqualType(): Type public function canAccessConstants(): TrinaryLogic { - return $this->isClassStringType(); + return $this->isClassString(); } public function hasConstant(string $constantName): TrinaryLogic @@ -512,7 +549,7 @@ public function hasConstant(string $constantName): TrinaryLogic return $this->getObjectType()->hasConstant($constantName); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { return $this->getObjectType()->getConstant($constantName); } @@ -528,15 +565,7 @@ public function toPhpDocNode(): TypeNode return $this->generalize(GeneralizePrecision::moreSpecific())->toPhpDocNode(); } - return new ConstTypeNode(new QuoteAwareConstExprStringNode($this->value, QuoteAwareConstExprStringNode::SINGLE_QUOTED)); - } - - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self($properties['value'], $properties['isClassString'] ?? false); + return new ConstTypeNode(new ConstExprStringNode($this->value, ConstExprStringNode::SINGLE_QUOTED)); } } diff --git a/src/Type/Constant/OversizedArrayBuilder.php b/src/Type/Constant/OversizedArrayBuilder.php index 663f1c1513..530fe86046 100644 --- a/src/Type/Constant/OversizedArrayBuilder.php +++ b/src/Type/Constant/OversizedArrayBuilder.php @@ -2,8 +2,10 @@ namespace PHPStan\Type\Constant; +use PhpParser\Node\ArrayItem; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Array_; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\TypeExpr; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryArrayListType; @@ -18,7 +20,8 @@ use function array_values; use function count; -class OversizedArrayBuilder +#[AutowiredService] +final class OversizedArrayBuilder { /** @@ -33,9 +36,6 @@ public function build(Array_ $expr, callable $getTypeCallback): Type $items = $expr->items; for ($i = 0; $i < count($items); $i++) { $item = $items[$i]; - if ($item === null) { - continue; - } if (!$item->unpack) { continue; } @@ -50,22 +50,19 @@ public function build(Array_ $expr, callable $getTypeCallback): Type } else { $keyExpr = new TypeExpr($innerKeyType); } - array_splice($items, $i++, 0, [new Expr\ArrayItem( + array_splice($items, $i++, 0, [new ArrayItem( new TypeExpr($innerValueType), $keyExpr, )]); } } else { - array_splice($items, $i, 1, [new Expr\ArrayItem( + array_splice($items, $i, 1, [new ArrayItem( new TypeExpr($valueType->getIterableValueType()), new TypeExpr($valueType->getIterableKeyType()), )]); } } foreach ($items as $item) { - if ($item === null) { - continue; - } if ($item->unpack) { throw new ShouldNotHappenException(); } @@ -97,7 +94,7 @@ public function build(Array_ $expr, callable $getTypeCallback): Type $arrayType = new ArrayType($keyType, $valueType); if ($isList) { - $arrayType = AccessoryArrayListType::intersectWith($arrayType); + $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 d5fab2b652..0000000000 --- a/src/Type/ConstantType.php +++ /dev/null @@ -1,9 +0,0 @@ -setBroker($broker); - } } /** diff --git a/src/Type/DynamicStaticMethodReturnTypeExtension.php b/src/Type/DynamicStaticMethodReturnTypeExtension.php index 87b74c9af4..e74f69460f 100644 --- a/src/Type/DynamicStaticMethodReturnTypeExtension.php +++ b/src/Type/DynamicStaticMethodReturnTypeExtension.php @@ -26,6 +26,7 @@ interface DynamicStaticMethodReturnTypeExtension { + /** @return class-string */ public function getClass(): string; public function isStaticMethodSupported(MethodReflection $methodReflection): bool; diff --git a/src/Type/Enum/EnumCaseObjectType.php b/src/Type/Enum/EnumCaseObjectType.php index 42d8f4afc2..302b93a8c8 100644 --- a/src/Type/Enum/EnumCaseObjectType.php +++ b/src/Type/Enum/EnumCaseObjectType.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Enum; +use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; @@ -16,6 +17,8 @@ use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\GeneralizePrecision; +use PHPStan\Type\IsSuperTypeOfResult; +use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\SubtractableType; use PHPStan\Type\Type; @@ -33,7 +36,7 @@ public function __construct( ?ClassReflection $classReflection = null, ) { - parent::__construct($className, null, $classReflection); + parent::__construct($className, classReflection: $classReflection); } public function getEnumCaseName(): string @@ -58,20 +61,15 @@ public function equals(Type $type): bool $this->getClassName() === $type->getClassName(); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { - return $this->acceptsWithReason($type, $strictTypes)->result; + return $this->isSuperTypeOf($type)->toAcceptsResult(); } - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult - { - return new AcceptsResult($this->isSuperTypeOf($type), []); - } - - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createFromBoolean( + return IsSuperTypeOfResult::createFromBoolean( $this->enumCaseName === $type->enumCaseName && $this->getClassName() === $type->getClassName(), ); } @@ -86,18 +84,18 @@ public function isSuperTypeOf(Type $type): TrinaryLogic ) { $isSuperType = $type->getSubtractedType()->isSuperTypeOf($this); if ($isSuperType->yes()) { - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } } $parent = new parent($this->getClassName(), $this->getSubtractedType(), $this->getClassReflection()); - return $parent->isSuperTypeOf($type)->and(TrinaryLogic::createMaybe()); + return $parent->isSuperTypeOf($type)->and(IsSuperTypeOfResult::createMaybe()); } public function subtract(Type $type): Type { - return $this; + return $this->changeSubtractedType($type); } public function getTypeWithoutSubtractedType(): Type @@ -107,7 +105,11 @@ public function getTypeWithoutSubtractedType(): Type public function changeSubtractedType(?Type $subtractedType): Type { - return $this; + if ($subtractedType === null || ! $this->equals($subtractedType)) { + return $this; + } + + return new NeverType(); } public function getSubtractedType(): ?Type @@ -133,7 +135,7 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember } if ($propertyName === 'name') { return new EnumUnresolvedPropertyPrototypeReflection( - new EnumPropertyReflection($classReflection, new ConstantStringType($this->enumCaseName)), + new EnumPropertyReflection($propertyName, $classReflection, new ConstantStringType($this->enumCaseName)), ); } @@ -146,7 +148,7 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember } return new EnumUnresolvedPropertyPrototypeReflection( - new EnumPropertyReflection($classReflection, $valueType), + new EnumPropertyReflection($propertyName, $classReflection, $valueType), ); } } @@ -179,12 +181,12 @@ public function generalize(GeneralizePrecision $precision): Type return new parent($this->getClassName(), null, $this->getClassReflection()); } - public function isSmallerThan(Type $otherType): TrinaryLogic + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { return TrinaryLogic::createNo(); } - public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -204,12 +206,4 @@ public function toPhpDocNode(): TypeNode ); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self($properties['className'], $properties['enumCaseName'], null); - } - } diff --git a/src/Type/ErrorType.php b/src/Type/ErrorType.php index 345465b306..751271aef3 100644 --- a/src/Type/ErrorType.php +++ b/src/Type/ErrorType.php @@ -41,12 +41,4 @@ public function equals(Type $type): bool return $type instanceof self; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self(); - } - } diff --git a/src/Type/ExponentiateHelper.php b/src/Type/ExponentiateHelper.php index 10da4dd484..fd65dc9e51 100644 --- a/src/Type/ExponentiateHelper.php +++ b/src/Type/ExponentiateHelper.php @@ -46,8 +46,7 @@ public static function exponentiate(Type $base, Type $exponent): Type } // exponentiation of a float, stays a float - $float = new FloatType(); - $isFloatBase = $float->isSuperTypeOf($base)->yes(); + $isFloatBase = $base->isFloat()->yes(); $isLooseZero = (new ConstantIntegerType(0))->isSuperTypeOf($exponent->toNumber()); if ($isLooseZero->yes()) { diff --git a/src/Type/ExpressionTypeResolverExtensionRegistry.php b/src/Type/ExpressionTypeResolverExtensionRegistry.php index 75c0d0b5ec..1d9dc436c0 100644 --- a/src/Type/ExpressionTypeResolverExtensionRegistry.php +++ b/src/Type/ExpressionTypeResolverExtensionRegistry.php @@ -2,7 +2,7 @@ namespace PHPStan\Type; -class ExpressionTypeResolverExtensionRegistry +final class ExpressionTypeResolverExtensionRegistry { /** diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index 3e0c73a93e..2ce66f1e1a 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -7,8 +7,11 @@ use PHPStan\Analyser\NameScope; use PHPStan\BetterReflection\Util\GetLastDocComment; use PHPStan\Broker\AnonymousClassNameHelper; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\File\FileHelper; use PHPStan\Parser\Parser; +use PHPStan\PhpDoc\NameScopeAlreadyBeingCreatedException; use PHPStan\PhpDoc\PhpDocNodeResolver; use PHPStan\PhpDoc\PhpDocStringResolver; use PHPStan\PhpDoc\ResolvedPhpDocBlock; @@ -38,7 +41,8 @@ use function str_contains; use function strtolower; -class FileTypeMapper +#[AutowiredService] +final class FileTypeMapper { private const SKIP_NODE = 1; @@ -59,6 +63,7 @@ class FileTypeMapper public function __construct( private ReflectionProviderProvider $reflectionProviderProvider, + #[AutowiredParameter(ref: '@defaultAnalysisParser')] private Parser $phpParser, private PhpDocStringResolver $phpDocStringResolver, private PhpDocNodeResolver $phpDocNodeResolver, @@ -99,6 +104,26 @@ public function getResolvedPhpDoc( 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])) { @@ -106,15 +131,15 @@ public function getResolvedPhpDoc( } if (isset($nameScopeMap[$nameScopeKey])) { - return $this->createResolvedPhpDocBlock($phpDocKey, $nameScopeMap[$nameScopeKey], $docComment, $fileName); + return $nameScopeMap[$nameScopeKey]; } if (!isset($this->inProcess[$fileName][$nameScopeKey])) { // wrong $fileName due to traits - return ResolvedPhpDocBlock::createEmpty(); + throw new NameScopeAlreadyBeingCreatedException(); } if ($this->inProcess[$fileName][$nameScopeKey] === true) { // PHPDoc has cyclic dependency - return ResolvedPhpDocBlock::createEmpty(); + throw new NameScopeAlreadyBeingCreatedException(); } if (is_callable($this->inProcess[$fileName][$nameScopeKey])) { @@ -123,7 +148,7 @@ public function getResolvedPhpDoc( $this->inProcess[$fileName][$nameScopeKey] = $resolveCallback(); } - return $this->createResolvedPhpDocBlock($phpDocKey, $this->inProcess[$fileName][$nameScopeKey], $docComment, $fileName); + return $this->inProcess[$fileName][$nameScopeKey]; } private function createResolvedPhpDocBlock(string $phpDocKey, NameScope $nameScope, string $phpDocString, ?string $fileName): ResolvedPhpDocBlock @@ -133,8 +158,7 @@ private function createResolvedPhpDocBlock(string $phpDocKey, NameScope $nameSco $this->resolvedPhpDocBlockCache = array_slice( $this->resolvedPhpDocBlockCache, 1, - null, - true, + preserve_keys: true, ); $this->resolvedPhpDocBlockCacheCount--; @@ -177,8 +201,7 @@ private function getNameScopeMap(string $fileName): array $this->memoryCache = array_slice( $this->memoryCache, 1, - null, - true, + preserve_keys: true, ); $this->memoryCacheCount--; } @@ -279,6 +302,11 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA } } elseif ($node instanceof Node\Stmt\Function_) { $functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); + } elseif ($node instanceof Node\PropertyHook) { + $propertyName = $node->getAttribute('propertyName'); + if ($propertyName !== null) { + $functionStack[] = sprintf('$%s::%s', $propertyName, $node->name->toString()); + } } $className = $classStack[count($classStack) - 1] ?? null; @@ -291,6 +319,17 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA $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; } @@ -376,6 +415,15 @@ static function (Node $node) use (&$namespace, &$functionStack, &$classStack): v } 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); + } } }, ); @@ -476,24 +524,26 @@ function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFoun } } elseif ($node instanceof Node\Stmt\Function_) { $functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); + } elseif ($node instanceof Node\PropertyHook) { + $propertyName = $node->getAttribute('propertyName'); + if ($propertyName !== null) { + $functionStack[] = sprintf('$%s::%s', $propertyName, $node->name->toString()); + } } $className = $classStack[count($classStack) - 1] ?? null; $functionName = $functionStack[count($functionStack) - 1] ?? null; $nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName); - if ($namespace === '') { - throw new ShouldNotHappenException('Namespace cannot be empty.'); - } - if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { + // property hook skipped on purpose, it does not support @template if (array_key_exists($nameScopeKey, $phpDocNodeMap)) { $phpDocNode = $phpDocNodeMap[$nameScopeKey]; $typeMapStack[] = function () use ($namespace, $uses, $className, $lookForTrait, $functionName, $phpDocNode, $typeMapStack, $typeAliasStack, $constUses): TemplateTypeMap { $typeMapCb = $typeMapStack[count($typeMapStack) - 1] ?? null; $currentTypeMap = $typeMapCb !== null ? $typeMapCb() : null; $typeAliasesMap = $typeAliasStack[count($typeAliasStack) - 1] ?? []; - $nameScope = new NameScope($namespace, $uses, $className, $functionName, $currentTypeMap, $typeAliasesMap, false, $constUses, $lookForTrait); + $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) { @@ -516,18 +566,20 @@ function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFoun $typeAliasesMap = $typeAliasStack[count($typeAliasStack) - 1] ?? []; if ( - $node instanceof Node\Stmt - && !$node instanceof Node\Stmt\Namespace_ - && !$node instanceof Node\Stmt\Declare_ - && !$node instanceof Node\Stmt\DeclareDeclare - && !$node instanceof Node\Stmt\Use_ - && !$node instanceof Node\Stmt\UseUse - && !$node instanceof Node\Stmt\GroupUse - && !$node instanceof Node\Stmt\TraitUse - && !$node instanceof Node\Stmt\TraitUseAdaptation - && !$node instanceof Node\Stmt\InlineHTML - && !($node instanceof Node\Stmt\Expression && $node->expr instanceof Node\Expr\Include_) - && !array_key_exists($nameScopeKey, $nameScopeMap) + ( + $node instanceof Node\PropertyHook + || ( + $node instanceof Node\Stmt + && !$node instanceof Node\Stmt\Namespace_ + && !$node instanceof Node\Stmt\Declare_ + && !$node instanceof Node\Stmt\Use_ + && !$node instanceof Node\Stmt\GroupUse + && !$node instanceof Node\Stmt\TraitUse + && !$node instanceof Node\Stmt\TraitUseAdaptation + && !$node instanceof Node\Stmt\InlineHTML + && !($node instanceof Node\Stmt\Expression && $node->expr instanceof Node\Expr\Include_) + ) + ) && !array_key_exists($nameScopeKey, $nameScopeMap) ) { $nameScopeMap[$nameScopeKey] = static fn (): NameScope => new NameScope( $namespace, @@ -536,13 +588,13 @@ function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFoun $functionName, ($typeMapCb !== null ? $typeMapCb() : TemplateTypeMap::createEmpty()), $typeAliasesMap, - false, - $constUses, - $lookForTrait, + constUses: $constUses, + typeAliasClassName: $lookForTrait, ); } 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; } @@ -710,6 +762,15 @@ static function (Node $node, $callbackResult) use (&$namespace, &$functionStack, } 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; diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index 2111d4d912..92ebb92cf1 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -7,6 +7,7 @@ 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; @@ -41,9 +42,6 @@ public function __construct() { } - /** - * @return string[] - */ public function getReferencedClasses(): array { return []; @@ -64,35 +62,30 @@ public function getConstantStrings(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof self || $type->isInteger()->yes()) { return AcceptsResult::createYes(); } if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } 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 @@ -129,6 +122,7 @@ public function toString(): Type { return new IntersectionType([ new StringType(), + new AccessoryUppercaseStringType(), new AccessoryNumericStringType(), ]); } @@ -139,8 +133,7 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], - [], - TrinaryLogic::createYes(), + isList: TrinaryLogic::createYes(), ); } @@ -149,6 +142,15 @@ 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(); @@ -229,7 +231,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -284,12 +296,4 @@ public function getFiniteTypes(): array return []; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self(); - } - } diff --git a/src/Type/GeneralizePrecision.php b/src/Type/GeneralizePrecision.php index 0cce10ed32..3f4d3be629 100644 --- a/src/Type/GeneralizePrecision.php +++ b/src/Type/GeneralizePrecision.php @@ -2,7 +2,7 @@ namespace PHPStan\Type; -class GeneralizePrecision +final class GeneralizePrecision { private const LESS_SPECIFIC = 1; @@ -40,11 +40,6 @@ public static function templateArgument(): self return self::create(self::TEMPLATE_ARGUMENT); } - public function isLessSpecific(): bool - { - return $this->value === self::LESS_SPECIFIC; - } - public function isMoreSpecific(): bool { return $this->value === self::MORE_SPECIFIC; diff --git a/src/Type/Generic/GenericClassStringType.php b/src/Type/Generic/GenericClassStringType.php index fd6ecd5f03..4e04a9df28 100644 --- a/src/Type/Generic/GenericClassStringType.php +++ b/src/Type/Generic/GenericClassStringType.php @@ -6,12 +6,12 @@ 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; @@ -60,14 +60,14 @@ public function describe(VerbosityLevel $level): string return sprintf('%s<%s>', parent::describe($level), $this->type->describe($level)); } - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } if ($type instanceof ConstantStringType) { - if (!$type->isClassStringType()->yes()) { + if (!$type->isClassString()->yes()) { return AcceptsResult::createNo(); } @@ -82,10 +82,10 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult return AcceptsResult::createNo(); } - return $this->type->acceptsWithReason($objectType, $strictTypes); + 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); @@ -94,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) { @@ -113,18 +113,18 @@ public function isSuperTypeOf(Type $type): TrinaryLogic $isSuperType = $genericType->isSuperTypeOf($objectType); } - if (!$type->isClassStringType()->yes()) { - $isSuperType = $isSuperType->and(TrinaryLogic::createMaybe()); + 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 @@ -157,7 +157,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap $typeToInfer = new ObjectType($receivedType->getValue()); } elseif ($receivedType instanceof self) { $typeToInfer = $receivedType->type; - } elseif ($receivedType->isClassStringType()->yes()) { + } elseif ($receivedType->isClassString()->yes()) { $typeToInfer = $this->type; if ($typeToInfer instanceof TemplateType) { $typeToInfer = $typeToInfer->getBound(); @@ -195,14 +195,6 @@ public function equals(Type $type): bool return true; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self($properties['type']); - } - public function toPhpDocNode(): TypeNode { return new GenericTypeNode( @@ -215,7 +207,7 @@ public function toPhpDocNode(): TypeNode public function tryRemove(Type $typeToRemove): ?Type { - if ($typeToRemove instanceof ConstantStringType && $typeToRemove->isClassStringType()->yes()) { + if ($typeToRemove instanceof ConstantStringType && $typeToRemove->isClassString()->yes()) { $generic = $this->getGenericType(); $genericObjectClassNames = $generic->getObjectClassNames(); diff --git a/src/Type/Generic/GenericObjectType.php b/src/Type/Generic/GenericObjectType.php index 775134aab6..a2bdadd7ae 100644 --- a/src/Type/Generic/GenericObjectType.php +++ b/src/Type/Generic/GenericObjectType.php @@ -8,16 +8,16 @@ use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; -use PHPStan\TrinaryLogic; use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; use PHPStan\Type\ErrorType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeWithClassName; @@ -91,9 +91,6 @@ public function equals(Type $type): bool return true; } - /** - * @return string[] - */ public function getReferencedClasses(): array { $classes = parent::getReferencedClasses(); @@ -118,32 +115,27 @@ public function getVariances(): array return $this->variances; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } - return $this->isSuperTypeOfInternal($type, true); + return $this->isSuperTypeOfInternal($type, true)->toAcceptsResult(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return $this->isSuperTypeOfInternal($type, false)->result; + return $this->isSuperTypeOfInternal($type, false); } - private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): AcceptsResult + private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): IsSuperTypeOfResult { - $nakedSuperTypeOf = new AcceptsResult(parent::isSuperTypeOf($type), []); + $nakedSuperTypeOf = parent::isSuperTypeOf($type); if ($nakedSuperTypeOf->no()) { return $nakedSuperTypeOf; } @@ -161,11 +153,11 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): Accept return $nakedSuperTypeOf; } - return $nakedSuperTypeOf->and(AcceptsResult::createMaybe()); + return $nakedSuperTypeOf->and(IsSuperTypeOfResult::createMaybe()); } if (count($this->types) !== count($ancestor->types)) { - return AcceptsResult::createNo(); + return IsSuperTypeOfResult::createNo(); } $classReflection = $this->getClassReflection(); @@ -192,19 +184,19 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): Accept $thisVariance = $this->variances[$i] ?? TemplateTypeVariance::createInvariant(); $ancestorVariance = $ancestor->variances[$i] ?? TemplateTypeVariance::createInvariant(); if (!$thisVariance->invariant()) { - $results[] = $thisVariance->isValidVarianceWithReason($templateType, $this->types[$i], $ancestor->types[$i]); + $results[] = $thisVariance->isValidVariance($templateType, $this->types[$i], $ancestor->types[$i]); } else { - $results[] = $templateType->isValidVarianceWithReason($this->types[$i], $ancestor->types[$i]); + $results[] = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i]); } - $results[] = AcceptsResult::createFromBoolean($thisVariance->validPosition($ancestorVariance)); + $results[] = IsSuperTypeOfResult::createFromBoolean($thisVariance->validPosition($ancestorVariance)); } if (count($results) === 0) { return $nakedSuperTypeOf; } - $result = AcceptsResult::createYes(); + $result = IsSuperTypeOfResult::createYes(); foreach ($results as $innerResult) { $result = $result->and($innerResult); } @@ -228,7 +220,7 @@ public function getClassReflection(): ?ClassReflection ->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(); } @@ -398,18 +390,4 @@ public function toPhpDocNode(): TypeNode ); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self( - $properties['className'], - $properties['types'], - $properties['subtractedType'] ?? null, - null, - $properties['variances'] ?? [], - ); - } - } diff --git a/src/Type/Generic/GenericStaticType.php b/src/Type/Generic/GenericStaticType.php new file mode 100644 index 0000000000..61692b704d --- /dev/null +++ b/src/Type/Generic/GenericStaticType.php @@ -0,0 +1,272 @@ + $types + * @param array $variances + */ + public function __construct( + private ClassReflection $classReflection, + private array $types, + private ?Type $subtractedType, + private array $variances, + ) + { + if (count($this->types) === 0) { + throw new ShouldNotHappenException('Cannot create GenericStaticType with zero types.'); + } + parent::__construct($classReflection, $subtractedType); + } + + /** + * @return array + */ + public function getTypes(): array + { + return $this->types; + } + + /** @return array */ + public function getVariances(): array + { + return $this->variances; + } + + public function getStaticObjectType(): ObjectType + { + if ($this->staticObjectType === null) { + if ($this->classReflection->isGeneric()) { + return $this->staticObjectType = new GenericObjectType( + $this->classReflection->getName(), + $this->types, + $this->subtractedType, + $this->classReflection, + $this->variances, + ); + } + + return $this->staticObjectType = parent::getStaticObjectType(); + } + + return $this->staticObjectType; + } + + public function changeBaseClass(ClassReflection $classReflection): StaticType + { + if ($classReflection->getName() === $this->getClassName()) { + return $this; + } + + if (!$classReflection->isGeneric()) { + return new StaticType($classReflection); + } + + $templateTags = $this->getClassReflection()->getTemplateTags(); + $i = 0; + $indexedTypes = []; + $indexedVariances = []; + foreach ($templateTags as $typeName => $tag) { + if (!array_key_exists($i, $this->types)) { + break; + } + if (!array_key_exists($i, $this->variances)) { + break; + } + $indexedTypes[$typeName] = $this->types[$i]; + $indexedVariances[$typeName] = $this->variances[$i]; + $i++; + } + + $newType = new GenericObjectType($classReflection->getName(), $classReflection->typeMapToList($classReflection->getTemplateTypeMap())); + $ancestorType = $newType->getAncestorWithClassName($this->getClassName()); + if ($ancestorType === null) { + return new self( + $classReflection, + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + $this->subtractedType, + $classReflection->varianceMapToList($classReflection->getCallSiteVarianceMap()), + ); + } + + $ancestorClassReflection = $ancestorType->getClassReflection(); + if ($ancestorClassReflection === null) { + return new self( + $classReflection, + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + $this->subtractedType, + $classReflection->varianceMapToList($classReflection->getCallSiteVarianceMap()), + ); + } + + $newClassTypes = []; + $newClassVariances = []; + foreach ($ancestorClassReflection->getActiveTemplateTypeMap()->getTypes() as $typeName => $templateType) { + if (!$templateType instanceof TemplateType) { + continue; + } + + if (!array_key_exists($typeName, $indexedTypes)) { + continue; + } + + $newClassTypes[$templateType->getName()] = $indexedTypes[$typeName]; + $newClassVariances[$templateType->getName()] = $indexedVariances[$typeName]; + } + + return new self($classReflection, $classReflection->typeMapToList(new TemplateTypeMap($newClassTypes)), $this->subtractedType, $classReflection->varianceMapToList(new TemplateTypeVarianceMap($newClassVariances))); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + if ($type instanceof self) { + return $this->getStaticObjectType()->isSuperTypeOf($type->getStaticObjectType()); + } + + return parent::isSuperTypeOf($type)->and(IsSuperTypeOfResult::createMaybe()); + } + + public function traverse(callable $cb): Type + { + $subtractedType = $this->getSubtractedType() !== null ? $cb($this->getSubtractedType()) : null; + + $typesChanged = false; + $types = []; + foreach ($this->types as $type) { + $newType = $cb($type); + $types[] = $newType; + if ($newType === $type) { + continue; + } + + $typesChanged = true; + } + + if ($subtractedType !== $this->getSubtractedType() || $typesChanged) { + return new self( + $this->classReflection, + $types, + $subtractedType, + $this->variances, + ); + } + + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof TypeWithClassName) { + return $this; + } + + $ancestor = $right->getAncestorWithClassName($this->getClassName()); + if (!$ancestor instanceof self) { + return $this; + } + + if (count($this->types) !== count($ancestor->types)) { + return $this; + } + + $typesChanged = false; + $types = []; + foreach ($this->types as $i => $leftType) { + $rightType = $ancestor->types[$i]; + $newType = $cb($leftType, $rightType); + $types[] = $newType; + if ($newType === $leftType) { + continue; + } + + $typesChanged = true; + } + + if ($typesChanged) { + return new self( + $this->classReflection, + $types, + null, + $this->variances, + ); + } + + return $this; + } + + public function changeSubtractedType(?Type $subtractedType): Type + { + if ($subtractedType !== null) { + $classReflection = $this->getClassReflection(); + if ($classReflection->getAllowedSubTypes() !== null) { + $objectType = $this->getStaticObjectType()->changeSubtractedType($subtractedType); + if ($objectType instanceof NeverType) { + return $objectType; + } + + if ($objectType instanceof ObjectType && $objectType->getSubtractedType() !== null) { + return new self($classReflection, $this->types, $objectType->getSubtractedType(), $this->variances); + } + + return TypeCombinator::intersect($this, $objectType); + } + } + + return new self( + $this->classReflection, + $this->types, + $subtractedType, + $this->variances, + ); + } + + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap + { + return $this->getStaticObjectType()->inferTemplateTypes($receivedType); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return $this->getStaticObjectType()->getReferencedTemplateTypes($positionVariance); + } + + public function toPhpDocNode(): TypeNode + { + /** @var IdentifierTypeNode $parent */ + $parent = parent::toPhpDocNode(); + return new GenericTypeNode( + $parent, + array_map(static fn (Type $type) => $type->toPhpDocNode(), $this->types), + array_map(static fn (TemplateTypeVariance $variance) => $variance->toPhpDocNodeVariance(), $this->variances), + ); + } + +} diff --git a/src/Type/Generic/TemplateArrayType.php b/src/Type/Generic/TemplateArrayType.php index bb0192d02b..e6658632cd 100644 --- a/src/Type/Generic/TemplateArrayType.php +++ b/src/Type/Generic/TemplateArrayType.php @@ -4,6 +4,7 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateArrayType extends ArrayType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ArrayType $bound, + ?Type $default, ) { parent::__construct($bound->getKeyType(), $bound->getItemType()); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateBenevolentUnionType.php b/src/Type/Generic/TemplateBenevolentUnionType.php index 3107542f29..aea8573131 100644 --- a/src/Type/Generic/TemplateBenevolentUnionType.php +++ b/src/Type/Generic/TemplateBenevolentUnionType.php @@ -21,6 +21,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, BenevolentUnionType $bound, + ?Type $default, ) { parent::__construct($bound->getTypes()); @@ -30,6 +31,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } /** @param Type[] $types */ @@ -41,7 +43,25 @@ public function withTypes(array $types): self $this->variance, $this->name, new BenevolentUnionType($types), + $this->default, ); } + public function filterTypes(callable $filterCb): Type + { + $result = parent::filterTypes($filterCb); + if (!$result instanceof TemplateType) { + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $result, + $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), + ); + } + + return $result; + } + } diff --git a/src/Type/Generic/TemplateBooleanType.php b/src/Type/Generic/TemplateBooleanType.php index 66ce5db4ec..27fc50f21b 100644 --- a/src/Type/Generic/TemplateBooleanType.php +++ b/src/Type/Generic/TemplateBooleanType.php @@ -4,6 +4,7 @@ use PHPStan\Type\BooleanType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateBooleanType extends BooleanType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, BooleanType $bound, + ?Type $default, ) { parent::__construct(); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateConstantArrayType.php b/src/Type/Generic/TemplateConstantArrayType.php index b291dab577..53ea994935 100644 --- a/src/Type/Generic/TemplateConstantArrayType.php +++ b/src/Type/Generic/TemplateConstantArrayType.php @@ -4,6 +4,7 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateConstantArrayType extends ConstantArrayType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ConstantArrayType $bound, + ?Type $default, ) { parent::__construct($bound->getKeyTypes(), $bound->getValueTypes(), $bound->getNextAutoIndexes(), $bound->getOptionalKeys(), $bound->isList()); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateConstantIntegerType.php b/src/Type/Generic/TemplateConstantIntegerType.php index e411af4edc..a4bc35b848 100644 --- a/src/Type/Generic/TemplateConstantIntegerType.php +++ b/src/Type/Generic/TemplateConstantIntegerType.php @@ -4,6 +4,7 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateConstantIntegerType extends ConstantIntegerType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ConstantIntegerType $bound, + ?Type $default, ) { parent::__construct($bound->getValue()); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateConstantStringType.php b/src/Type/Generic/TemplateConstantStringType.php index bcb3b0cf94..f4d3b8dbbb 100644 --- a/src/Type/Generic/TemplateConstantStringType.php +++ b/src/Type/Generic/TemplateConstantStringType.php @@ -4,6 +4,7 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateConstantStringType extends ConstantStringType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ConstantStringType $bound, + ?Type $default, ) { parent::__construct($bound->getValue()); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateFloatType.php b/src/Type/Generic/TemplateFloatType.php index 32332bb3ef..b3df6ccd2e 100644 --- a/src/Type/Generic/TemplateFloatType.php +++ b/src/Type/Generic/TemplateFloatType.php @@ -4,6 +4,7 @@ use PHPStan\Type\FloatType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateFloatType extends FloatType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, FloatType $bound, + ?Type $default, ) { parent::__construct(); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateGenericObjectType.php b/src/Type/Generic/TemplateGenericObjectType.php index 3810841ec9..8236a7094e 100644 --- a/src/Type/Generic/TemplateGenericObjectType.php +++ b/src/Type/Generic/TemplateGenericObjectType.php @@ -22,15 +22,17 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, GenericObjectType $bound, + ?Type $default, ) { - parent::__construct($bound->getClassName(), $bound->getTypes(), null, null, $bound->getVariances()); + 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; } protected function recreate(string $className, array $types, ?Type $subtractedType, array $variances = []): GenericObjectType @@ -41,6 +43,7 @@ protected function recreate(string $className, array $types, ?Type $subtractedTy $this->variance, $this->name, $this->getBound(), + $this->default, ); } diff --git a/src/Type/Generic/TemplateIntegerType.php b/src/Type/Generic/TemplateIntegerType.php index 64c631980d..b4057fa327 100644 --- a/src/Type/Generic/TemplateIntegerType.php +++ b/src/Type/Generic/TemplateIntegerType.php @@ -4,6 +4,7 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateIntegerType extends IntegerType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, IntegerType $bound, + ?Type $default, ) { parent::__construct(); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateIntersectionType.php b/src/Type/Generic/TemplateIntersectionType.php index 87f1ca18a7..7576541dbc 100644 --- a/src/Type/Generic/TemplateIntersectionType.php +++ b/src/Type/Generic/TemplateIntersectionType.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Generic; use PHPStan\Type\IntersectionType; +use PHPStan\Type\Type; /** @api */ final class TemplateIntersectionType extends IntersectionType implements TemplateType @@ -20,6 +21,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, IntersectionType $bound, + ?Type $default, ) { parent::__construct($bound->getTypes()); @@ -29,6 +31,7 @@ public function __construct( $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 index 7312ea2ef4..d8522eb503 100644 --- a/src/Type/Generic/TemplateKeyOfType.php +++ b/src/Type/Generic/TemplateKeyOfType.php @@ -23,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, KeyOfType $bound, + ?Type $default, ) { parent::__construct($bound->getType()); @@ -31,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function getResult(): Type @@ -43,6 +45,7 @@ protected function getResult(): Type $result, $this->getVariance(), $this->getStrategy(), + $this->getDefault(), ); } diff --git a/src/Type/Generic/TemplateMixedType.php b/src/Type/Generic/TemplateMixedType.php index 3363818673..8160633fc3 100644 --- a/src/Type/Generic/TemplateMixedType.php +++ b/src/Type/Generic/TemplateMixedType.php @@ -2,8 +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; @@ -24,6 +24,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, MixedType $bound, + ?Type $default, ) { parent::__construct(true); @@ -33,21 +34,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->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; - } - - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult - { - $isSuperType = new AcceptsResult($this->isSuperTypeOf($acceptingType), []); + $isSuperType = $this->isSuperTypeOf($acceptingType)->toAcceptsResult(); if ($isSuperType->no()) { return $isSuperType; } @@ -62,6 +59,7 @@ public function toStrictMixedType(): TemplateStrictMixedType $this->variance, $this->name, new StrictMixedType(), + $this->default, ); } diff --git a/src/Type/Generic/TemplateObjectShapeType.php b/src/Type/Generic/TemplateObjectShapeType.php index 5b1f187c6d..270af37931 100644 --- a/src/Type/Generic/TemplateObjectShapeType.php +++ b/src/Type/Generic/TemplateObjectShapeType.php @@ -4,6 +4,7 @@ use PHPStan\Type\ObjectShapeType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateObjectShapeType extends ObjectShapeType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ObjectShapeType $bound, + ?Type $default, ) { parent::__construct($bound->getProperties(), $bound->getOptionalProperties()); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateObjectType.php b/src/Type/Generic/TemplateObjectType.php index a67aa723dd..220414ca14 100644 --- a/src/Type/Generic/TemplateObjectType.php +++ b/src/Type/Generic/TemplateObjectType.php @@ -4,6 +4,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateObjectType extends ObjectType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ObjectType $bound, + ?Type $default, ) { parent::__construct($bound->getClassName()); @@ -31,6 +33,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateObjectWithoutClassType.php b/src/Type/Generic/TemplateObjectWithoutClassType.php index 3d3cb9e8ca..7d6aebc6f9 100644 --- a/src/Type/Generic/TemplateObjectWithoutClassType.php +++ b/src/Type/Generic/TemplateObjectWithoutClassType.php @@ -4,6 +4,7 @@ use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ class TemplateObjectWithoutClassType extends ObjectWithoutClassType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ObjectWithoutClassType $bound, + ?Type $default, ) { parent::__construct(); @@ -31,6 +33,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateStrictMixedType.php b/src/Type/Generic/TemplateStrictMixedType.php index 6ae56cc228..6feefd8696 100644 --- a/src/Type/Generic/TemplateStrictMixedType.php +++ b/src/Type/Generic/TemplateStrictMixedType.php @@ -2,8 +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; @@ -24,6 +24,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, StrictMixedType $bound, + ?Type $default, ) { $this->scope = $scope; @@ -31,21 +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->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; - } - - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult - { - return new AcceptsResult($this->isSubTypeOf($acceptingType), []); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } } diff --git a/src/Type/Generic/TemplateStringType.php b/src/Type/Generic/TemplateStringType.php index 084612c641..1ae72a3384 100644 --- a/src/Type/Generic/TemplateStringType.php +++ b/src/Type/Generic/TemplateStringType.php @@ -4,6 +4,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateStringType extends StringType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, StringType $bound, + ?Type $default, ) { parent::__construct(); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateType.php b/src/Type/Generic/TemplateType.php index 7661078ca1..2dfa5dafbd 100644 --- a/src/Type/Generic/TemplateType.php +++ b/src/Type/Generic/TemplateType.php @@ -2,9 +2,8 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; -use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\Type; /** @api */ @@ -18,13 +17,13 @@ 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 isValidVarianceWithReason(Type $a, Type $b): AcceptsResult; + public function isValidVariance(Type $a, Type $b): IsSuperTypeOfResult; public function getVariance(): TemplateTypeVariance; diff --git a/src/Type/Generic/TemplateTypeArgumentStrategy.php b/src/Type/Generic/TemplateTypeArgumentStrategy.php index 414ffc12aa..0e8f59cf5e 100644 --- a/src/Type/Generic/TemplateTypeArgumentStrategy.php +++ b/src/Type/Generic/TemplateTypeArgumentStrategy.php @@ -12,15 +12,15 @@ /** * 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): AcceptsResult { if ($right instanceof CompoundType) { - $accepts = $right->isAcceptedWithReasonBy($left, $strictTypes); + $accepts = $right->isAcceptedBy($left, $strictTypes); } else { - $accepts = $left->getBound()->acceptsWithReason($right, $strictTypes) + $accepts = $left->getBound()->accepts($right, $strictTypes) ->and(AcceptsResult::createMaybe()); if ($accepts->maybe()) { $verbosity = VerbosityLevel::getRecommendedLevelByType($left, $right); @@ -43,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 c29a175d2c..0471bc249c 100644 --- a/src/Type/Generic/TemplateTypeFactory.php +++ b/src/Type/Generic/TemplateTypeFactory.php @@ -12,6 +12,7 @@ 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\ObjectShapeType; @@ -28,91 +29,95 @@ final class TemplateTypeFactory /** * @param non-empty-string $name */ - public static function create(TemplateTypeScope $scope, string $name, ?Type $bound, TemplateTypeVariance $variance, ?TemplateTypeStrategy $strategy = null): TemplateType + public static function create(TemplateTypeScope $scope, string $name, ?Type $bound, TemplateTypeVariance $variance, ?TemplateTypeStrategy $strategy = null, ?Type $default = null): TemplateType { $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); + 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); + 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); + 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); } } if ($bound instanceof IntersectionType) { - return new TemplateIntersectionType($scope, $strategy, $variance, $name, $bound); + 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); + return new TemplateKeyOfType($scope, $strategy, $variance, $name, $bound, $default); } - return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true)); + if ($bound instanceof IterableType && ($boundClass === IterableType::class || $bound instanceof TemplateType)) { + return new TemplateIterableType($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 166464a884..29ecd48720 100644 --- a/src/Type/Generic/TemplateTypeHelper.php +++ b/src/Type/Generic/TemplateTypeHelper.php @@ -10,7 +10,7 @@ use PHPStan\Type\TypeTraverser; use PHPStan\Type\VerbosityLevel; -class TemplateTypeHelper +final class TemplateTypeHelper { /** @@ -45,7 +45,7 @@ public static function resolveTemplateTypes( } if ($newType instanceof ErrorType && !$keepErrorTypes) { - return $traverse($type->getBound()); + return $traverse($type->getDefault() ?? $type->getBound()); } $callSiteVariance = $callSiteVariances->getVariance($type->getName()); @@ -68,6 +68,17 @@ public static function resolveTemplateTypes( }); } + 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 { diff --git a/src/Type/Generic/TemplateTypeMap.php b/src/Type/Generic/TemplateTypeMap.php index f807e3d65e..49f3a08496 100644 --- a/src/Type/Generic/TemplateTypeMap.php +++ b/src/Type/Generic/TemplateTypeMap.php @@ -5,12 +5,15 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use function array_key_exists; use function count; -/** @api */ -class TemplateTypeMap +/** + * @api + */ +final class TemplateTypeMap { private static ?TemplateTypeMap $empty = null; @@ -208,18 +211,10 @@ public function resolveToBounds(): self if ($this->resolvedToBounds !== null) { return $this->resolvedToBounds; } - return $this->resolvedToBounds = $this->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::resolveToBounds($type)); - } - - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['types'], - $properties['lowerBoundTypes'] ?? [], - ); + return $this->resolvedToBounds = $this->map(static fn (string $name, Type $type): Type => TypeTraverser::map( + $type, + static fn (Type $type, callable $traverse): Type => $type instanceof TemplateType ? $traverse($type->getDefault() ?? $type->getBound()) : $traverse($type), + )); } } diff --git a/src/Type/Generic/TemplateTypeParameterStrategy.php b/src/Type/Generic/TemplateTypeParameterStrategy.php index 5ad7793665..3e18bccf2d 100644 --- a/src/Type/Generic/TemplateTypeParameterStrategy.php +++ b/src/Type/Generic/TemplateTypeParameterStrategy.php @@ -9,16 +9,16 @@ /** * 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): AcceptsResult { if ($right instanceof CompoundType) { - return $right->isAcceptedWithReasonBy($left, $strictTypes); + return $right->isAcceptedBy($left, $strictTypes); } - return $left->getBound()->acceptsWithReason($right, $strictTypes); + return $left->getBound()->accepts($right, $strictTypes); } public function isArgument(): bool @@ -26,12 +26,4 @@ public function isArgument(): bool return false; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self - { - return new self(); - } - } diff --git a/src/Type/Generic/TemplateTypeReference.php b/src/Type/Generic/TemplateTypeReference.php index 260abd3d93..0be67d5e08 100644 --- a/src/Type/Generic/TemplateTypeReference.php +++ b/src/Type/Generic/TemplateTypeReference.php @@ -2,7 +2,7 @@ namespace PHPStan\Type\Generic; -class TemplateTypeReference +final class TemplateTypeReference { public function __construct(private TemplateType $type, private TemplateTypeVariance $positionVariance) diff --git a/src/Type/Generic/TemplateTypeScope.php b/src/Type/Generic/TemplateTypeScope.php index e40eab90d3..f362ecadd4 100644 --- a/src/Type/Generic/TemplateTypeScope.php +++ b/src/Type/Generic/TemplateTypeScope.php @@ -4,7 +4,7 @@ use function sprintf; -class TemplateTypeScope +final class TemplateTypeScope { public static function createWithAnonymousFunction(): self @@ -68,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/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index 66fd32a2fd..a35451b64f 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -2,13 +2,12 @@ namespace PHPStan\Type\Generic; -use PHPStan\DependencyInjection\BleedingEdgeToggle; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; use PHPStan\Type\AcceptsResult; -use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\SubtractableType; @@ -37,6 +36,8 @@ trait TemplateTypeTrait /** @var TBound */ private Type $bound; + private ?Type $default; + /** @return non-empty-string */ public function getName(): string { @@ -54,6 +55,11 @@ 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 { @@ -63,10 +69,12 @@ public function describe(VerbosityLevel $level): string } else { $boundDescription = sprintf(' of %s', $this->bound->describe($level)); } + $defaultDescription = $this->default !== null ? sprintf(' = %s', $this->default->describe($level)) : ''; return sprintf( - '%s%s', + '%s%s%s', $this->name, $boundDescription, + $defaultDescription, ); }; @@ -90,17 +98,13 @@ public function toArgument(): TemplateType $this->variance, $this->name, TemplateTypeHelper::toArgument($this->getBound()), + $this->default !== null ? TemplateTypeHelper::toArgument($this->default) : null, ); } - public function isValidVariance(Type $a, Type $b): TrinaryLogic + public function isValidVariance(Type $a, Type $b): IsSuperTypeOfResult { - return $this->isValidVarianceWithReason($a, $b)->result; - } - - public function isValidVarianceWithReason(Type $a, Type $b): AcceptsResult - { - return $this->variance->isValidVarianceWithReason($this, $a, $b); + return $this->variance->isValidVariance($this, $a, $b); } public function subtract(Type $typeToRemove): Type @@ -112,6 +116,7 @@ public function subtract(Type $typeToRemove): Type $removedBound, $this->getVariance(), $this->getStrategy(), + $this->getDefault(), ); } @@ -128,6 +133,7 @@ public function getTypeWithoutSubtractedType(): Type $bound->getTypeWithoutSubtractedType(), $this->getVariance(), $this->getStrategy(), + $this->getDefault(), ); } @@ -144,6 +150,7 @@ public function changeSubtractedType(?Type $subtractedType): Type $bound->changeSubtractedType($subtractedType), $this->getVariance(), $this->getStrategy(), + $this->getDefault(), ); } @@ -162,15 +169,14 @@ public function equals(Type $type): bool return $type instanceof self && $type->scope->equals($this->scope) && $type->name === $this->name - && $this->bound->equals($type->bound); - } - - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic - { - return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + && $this->bound->equals($type->bound) + && ( + ($this->default === null && $type->default === null) + || ($this->default !== null && $type->default !== null && $this->default->equals($type->default)) + ); } - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { /** @var TBound $bound */ $bound = $this->getBound(); @@ -180,46 +186,41 @@ public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): && !$acceptingType instanceof TemplateType && ($acceptingType instanceof UnionType || $acceptingType instanceof IntersectionType) ) { - return $acceptingType->acceptsWithReason($this, $strictTypes); + return $acceptingType->accepts($this, $strictTypes); } if (!$acceptingType instanceof TemplateType) { - return $acceptingType->acceptsWithReason($this->getBound(), $strictTypes); + return $acceptingType->accepts($this->getBound(), $strictTypes); } if ($this->getScope()->equals($acceptingType->getScope()) && $this->getName() === $acceptingType->getName()) { - return $acceptingType->getBound()->acceptsWithReason($this->getBound(), $strictTypes); + return $acceptingType->getBound()->accepts($this->getBound(), $strictTypes); } - return $acceptingType->getBound()->acceptsWithReason($this->getBound(), $strictTypes) + return $acceptingType->getBound()->accepts($this->getBound(), $strictTypes) ->and(new AcceptsResult(TrinaryLogic::createMaybe(), [])); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { return $this->strategy->accepts($this, $type, $strictTypes); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof TemplateType || $type instanceof IntersectionType) { return $type->isSubTypeOf($this); } if ($type instanceof NeverType) { - return TrinaryLogic::createYes(); + 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 TBound $bound */ $bound = $this->getBound(); @@ -241,7 +242,7 @@ public function isSubTypeOf(Type $type): TrinaryLogic } return $type->getBound()->isSuperTypeOf($this->getBound()) - ->and(TrinaryLogic::createMaybe()); + ->and(IsSuperTypeOfResult::createMaybe()); } public function toArrayKey(): Type @@ -249,6 +250,11 @@ public function toArrayKey(): Type return $this; } + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { if ( @@ -268,13 +274,6 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap TemplateTypeVariance::createStatic(), )); if ($resolvedBound->isSuperTypeOf($receivedType)->yes()) { - if (!BleedingEdgeToggle::isBleedingEdge() && $this->shouldGeneralizeInferredType()) { - $generalizedType = $receivedType->generalize(GeneralizePrecision::templateArgument()); - if ($resolvedBound->isSuperTypeOf($generalizedType)->yes()) { - $receivedType = $generalizedType; - } - } - return (new TemplateTypeMap([ $this->name => $receivedType, ]))->union($map); @@ -298,15 +297,12 @@ public function getStrategy(): TemplateTypeStrategy return $this->strategy; } - protected function shouldGeneralizeInferredType(): bool - { - return true; - } - public function traverse(callable $cb): Type { $bound = $cb($this->getBound()); - if ($this->getBound() === $bound) { + $default = $this->getDefault() !== null ? $cb($this->getDefault()) : null; + + if ($this->getBound() === $bound && $this->getDefault() === $default) { return $this; } @@ -316,6 +312,7 @@ public function traverse(callable $cb): Type $bound, $this->getVariance(), $this->getStrategy(), + $default, ); } @@ -326,7 +323,9 @@ public function traverseSimultaneously(Type $right, callable $cb): Type } $bound = $cb($this->getBound(), $right->getBound()); - if ($this->getBound() === $bound) { + $default = $this->getDefault() !== null && $right->getDefault() !== null ? $cb($this->getDefault(), $right->getDefault()) : null; + + if ($this->getBound() === $bound && $this->getDefault() === $default) { return $this; } @@ -336,6 +335,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type $bound, $this->getVariance(), $this->getStrategy(), + $default, ); } @@ -352,6 +352,7 @@ public function tryRemove(Type $typeToRemove): ?Type $bound, $this->getVariance(), $this->getStrategy(), + $this->getDefault(), ); } @@ -360,18 +361,4 @@ public function toPhpDocNode(): TypeNode return new IdentifierTypeNode($this->name); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self( - $properties['scope'], - $properties['strategy'], - $properties['variance'], - $properties['name'], - $properties['bound'], - ); - } - } diff --git a/src/Type/Generic/TemplateTypeVariance.php b/src/Type/Generic/TemplateTypeVariance.php index a3ab7a04bd..a630895bed 100644 --- a/src/Type/Generic/TemplateTypeVariance.php +++ b/src/Type/Generic/TemplateTypeVariance.php @@ -5,15 +5,17 @@ use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; -use PHPStan\Type\AcceptsResult; 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; @@ -25,8 +27,6 @@ class TemplateTypeVariance /** @var self[] */ private static array $registry; - private static bool $invarianceCompositionEnabled = false; - private function __construct(private int $value) { } @@ -115,7 +115,7 @@ public function compose(self $other): self return self::createInvariant(); } - if (self::$invarianceCompositionEnabled && $this->invariant()) { + if ($this->invariant()) { return self::createInvariant(); } @@ -126,35 +126,30 @@ public function compose(self $other): self return $other; } - public function isValidVariance(Type $a, Type $b): TrinaryLogic - { - return $this->isValidVarianceWithReason(null, $a, $b)->result; - } - - public function isValidVarianceWithReason(?TemplateType $templateType, Type $a, Type $b): AcceptsResult + public function isValidVariance(TemplateType $templateType, Type $a, Type $b): IsSuperTypeOfResult { if ($b instanceof NeverType) { - return AcceptsResult::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($a instanceof MixedType && !$a instanceof TemplateType) { - return AcceptsResult::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($a instanceof BenevolentUnionType) { if (!$a->isSuperTypeOf($b)->no()) { - return AcceptsResult::createYes(); + return IsSuperTypeOfResult::createYes(); } } if ($b instanceof BenevolentUnionType) { if (!$b->isSuperTypeOf($a)->no()) { - return AcceptsResult::createYes(); + return IsSuperTypeOfResult::createYes(); } } if ($b instanceof MixedType && !$b instanceof TemplateType) { - return AcceptsResult::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($this->invariant()) { @@ -162,8 +157,7 @@ public function isValidVarianceWithReason(?TemplateType $templateType, Type $a, $reasons = []; if (!$result) { if ( - $templateType !== null - && $templateType->getScope()->getClassName() !== null + $templateType->getScope()->getClassName() !== null && $a->isSuperTypeOf($b)->yes() ) { $reasons[] = sprintf( @@ -174,19 +168,19 @@ public function isValidVarianceWithReason(?TemplateType $templateType, Type $a, } } - return new AcceptsResult(TrinaryLogic::createFromBoolean($result), $reasons); + return new IsSuperTypeOfResult(TrinaryLogic::createFromBoolean($result), $reasons); } if ($this->covariant()) { - return new AcceptsResult($a->isSuperTypeOf($b), []); + return $a->isSuperTypeOf($b); } if ($this->contravariant()) { - return new AcceptsResult($b->isSuperTypeOf($a), []); + return $b->isSuperTypeOf($a); } if ($this->bivariant()) { - return AcceptsResult::createYes(); + return IsSuperTypeOfResult::createYes(); } throw new ShouldNotHappenException(); @@ -242,17 +236,4 @@ public function toPhpDocNodeVariance(): string throw new ShouldNotHappenException(); } - /** - * @param array{value: int} $properties - */ - public static function __set_state(array $properties): self - { - return new self($properties['value']); - } - - public static function setInvarianceCompositionEnabled(bool $enabled): void - { - self::$invarianceCompositionEnabled = $enabled; - } - } diff --git a/src/Type/Generic/TemplateTypeVarianceMap.php b/src/Type/Generic/TemplateTypeVarianceMap.php index 55d3a18aa3..072c7952e5 100644 --- a/src/Type/Generic/TemplateTypeVarianceMap.php +++ b/src/Type/Generic/TemplateTypeVarianceMap.php @@ -4,8 +4,10 @@ use function array_key_exists; -/** @api */ -class TemplateTypeVarianceMap +/** + * @api + */ +final class TemplateTypeVarianceMap { private static ?TemplateTypeVarianceMap $empty = null; diff --git a/src/Type/Generic/TemplateUnionType.php b/src/Type/Generic/TemplateUnionType.php index 997fb21238..dc58af565a 100644 --- a/src/Type/Generic/TemplateUnionType.php +++ b/src/Type/Generic/TemplateUnionType.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Generic; +use PHPStan\Type\Type; use PHPStan\Type\UnionType; /** @api */ @@ -20,6 +21,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, UnionType $bound, + ?Type $default, ) { parent::__construct($bound->getTypes()); @@ -29,6 +31,24 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; + } + + public function filterTypes(callable $filterCb): Type + { + $result = parent::filterTypes($filterCb); + if (!$result instanceof TemplateType) { + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $result, + $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), + ); + } + + return $result; } } diff --git a/src/Type/Generic/TypeProjectionHelper.php b/src/Type/Generic/TypeProjectionHelper.php index 217103bd09..c311cde2f8 100644 --- a/src/Type/Generic/TypeProjectionHelper.php +++ b/src/Type/Generic/TypeProjectionHelper.php @@ -6,7 +6,7 @@ use PHPStan\Type\VerbosityLevel; use function sprintf; -class TypeProjectionHelper +final class TypeProjectionHelper { public static function describe( diff --git a/src/Type/GenericTypeVariableResolver.php b/src/Type/GenericTypeVariableResolver.php deleted file mode 100644 index 4f13c5eec9..0000000000 --- a/src/Type/GenericTypeVariableResolver.php +++ /dev/null @@ -1,50 +0,0 @@ -getClassReflection(); - if ($classReflection === null) { - return null; - } - $ancestorClassReflection = $classReflection->getAncestorWithClassName($genericClassName); - if ($ancestorClassReflection === null) { - return null; - } - - $activeTemplateTypeMap = $ancestorClassReflection->getPossiblyIncompleteActiveTemplateTypeMap(); - - $type = $activeTemplateTypeMap->getType($typeVariableName); - if ($type instanceof ErrorType) { - $templateTypeMap = $ancestorClassReflection->getTemplateTypeMap(); - $templateType = $templateTypeMap->getType($typeVariableName); - if ($templateType === null) { - return $type; - } - - $bound = TemplateTypeHelper::resolveToBounds($templateType); - if ($bound instanceof MixedType && $bound->isExplicitMixed()) { - return new MixedType(false); - } - - return $bound; - } - - return $type; - } - -} diff --git a/src/Type/Helper/GetTemplateTypeType.php b/src/Type/Helper/GetTemplateTypeType.php index 12e03fe1b0..c45a7be5fa 100644 --- a/src/Type/Helper/GetTemplateTypeType.php +++ b/src/Type/Helper/GetTemplateTypeType.php @@ -2,7 +2,7 @@ namespace PHPStan\Type\Helper; -use PHPStan\PhpDocParser\Ast\ConstExpr\QuoteAwareConstExprStringNode; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; @@ -98,21 +98,9 @@ public function toPhpDocNode(): TypeNode [ $this->type->toPhpDocNode(), new IdentifierTypeNode($this->ancestorClassName), - new ConstTypeNode(new QuoteAwareConstExprStringNode($this->templateTypeName, QuoteAwareConstExprStringNode::SINGLE_QUOTED)), + new ConstTypeNode(new ConstExprStringNode($this->templateTypeName, ConstExprStringNode::SINGLE_QUOTED)), ], ); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self( - $properties['type'], - $properties['ancestorClassName'], - $properties['templateTypeName'], - ); - } - } diff --git a/src/Type/IntegerRangeType.php b/src/Type/IntegerRangeType.php index b7962f71ae..1c956015dd 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -2,6 +2,7 @@ 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; @@ -9,11 +10,14 @@ 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; @@ -200,25 +204,20 @@ public function shift(int $amount): Type return self::fromInterval($min, $max); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof parent) { - return new AcceptsResult($this->isSuperTypeOf($type), []); + return $this->isSuperTypeOf($type)->toAcceptsResult(); } if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } 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) { @@ -230,48 +229,48 @@ 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) { - return $this->isSubTypeOfUnion($otherType); + return $this->isSubTypeOfUnionWithReason($otherType); } if ($otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } - private function isSubTypeOfUnion(UnionType $otherType): TrinaryLogic + private function isSubTypeOfUnionWithReason(UnionType $otherType): IsSuperTypeOfResult { if ($this->min !== null && $this->max !== null) { $matchingConstantIntegers = array_filter( @@ -280,21 +279,16 @@ private function isSubTypeOfUnion(UnionType $otherType): TrinaryLogic ); if (count($matchingConstantIntegers) === ($this->max - $this->min + 1)) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } } - return TrinaryLogic::createNo()->lazyOr($otherType->getTypes(), fn (Type $innerType) => $this->isSubTypeOf($innerType)); + return IsSuperTypeOfResult::createNo()->or(...array_map(fn (Type $innerType) => $this->isSubTypeOf($innerType), $otherType->getTypes())); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; - } - - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult - { - return new AcceptsResult($this->isSubTypeOf($acceptingType), []); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -307,75 +301,115 @@ public function generalize(GeneralizePrecision $precision): Type 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), @@ -388,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 = []; @@ -399,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(), @@ -417,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 = []; @@ -464,10 +498,17 @@ public function toAbsoluteNumber(): Type 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(), ]); @@ -475,6 +516,8 @@ public function toString(): Type return new IntersectionType([ new StringType(), + new AccessoryLowercaseStringType(), + new AccessoryUppercaseStringType(), new AccessoryNumericStringType(), ]); } @@ -689,12 +732,26 @@ public function toPhpDocNode(): TypeNode return new GenericTypeNode(new IdentifierTypeNode('int'), [$min, $max]); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { - return new self($properties['min'], $properties['max']); + $zeroInt = new ConstantIntegerType(0); + if ($zeroInt->isSuperTypeOf($this)->no()) { + if ($type->isTrue()->yes()) { + return new ConstantBooleanType(true); + } + if ($type->isFalse()->yes()) { + return new ConstantBooleanType(false); + } + } + + if ( + $this->isSmallerThan($type, $phpVersion)->yes() + || $this->isGreaterThan($type, $phpVersion)->yes() + ) { + return new ConstantBooleanType(false); + } + + return parent::looseCompare($type, $phpVersion); } } diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index f91a646c9d..0dd713bee8 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -6,8 +6,11 @@ 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; @@ -49,14 +52,6 @@ public function getConstantStrings(): array return []; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self(); - } - public function toNumber(): Type { return $this; @@ -81,6 +76,8 @@ public function toString(): Type { return new IntersectionType([ new StringType(), + new AccessoryLowercaseStringType(), + new AccessoryUppercaseStringType(), new AccessoryNumericStringType(), ]); } @@ -91,8 +88,7 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], - [], - TrinaryLogic::createYes(), + isList: TrinaryLogic::createYes(), ); } @@ -101,6 +97,15 @@ 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(); @@ -143,6 +148,18 @@ public function isScalar(): TrinaryLogic 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(); } diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 9b4fbf22ba..2d305b6883 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -3,15 +3,16 @@ 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\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\IntersectionTypeUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\IntersectionTypeUnresolvedPropertyPrototypeReflection; @@ -21,17 +22,24 @@ 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; @@ -40,12 +48,14 @@ 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 str_starts_with; +use function strcasecmp; use function strlen; use function substr; +use function usort; /** @api */ class IntersectionType implements CompoundType @@ -178,16 +188,11 @@ public function getConstantStrings(): array return $strings; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $otherType, bool $strictTypes): AcceptsResult + public function accepts(Type $otherType, bool $strictTypes): AcceptsResult { $result = AcceptsResult::createYes(); foreach ($this->types as $type) { - $result = $result->and($type->acceptsWithReason($otherType, $strictTypes)); + $result = $result->and($type->accepts($otherType, $strictTypes)); } if (!$result->yes()) { @@ -219,43 +224,38 @@ public function acceptsWithReason(Type $otherType, bool $strictTypes): AcceptsRe return $result; } - public function isSuperTypeOf(Type $otherType): TrinaryLogic + public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof IntersectionType && $this->equals($otherType)) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($otherType instanceof NeverType) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createYes()->lazyAnd($this->getTypes(), static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType)); + return IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType), $this->types)); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if (($otherType instanceof self || $otherType instanceof UnionType) && !$otherType instanceof TemplateType) { return $otherType->isSuperTypeOf($this); } - $result = TrinaryLogic::lazyMaxMin($this->getTypes(), static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType)); + $result = IsSuperTypeOfResult::maxMin(...array_map(static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType), $this->types)); if ($this->isOversizedArray()->yes()) { if (!$result->no()) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } } return $result; } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; - } - - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult - { - $result = AcceptsResult::maxMin(...array_map(static fn (Type $innerType) => $acceptingType->acceptsWithReason($innerType, $strictTypes), $this->types)); + $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(); @@ -301,13 +301,43 @@ public function describe(VerbosityLevel $level): string return $level->handle( function () use ($level): string { $typeNames = []; + $isList = $this->isList()->yes(); + $valueType = null; foreach ($this->getSortedTypes() as $type) { + if ($isList) { + if ($type instanceof ArrayType || $type instanceof ConstantArrayType) { + $valueType = $type->getIterableValueType(); + continue; + } + if ($type instanceof NonEmptyArrayType) { + continue; + } + } if ($type instanceof AccessoryType) { continue; } $typeNames[] = $type->generalize(GeneralizePrecision::lessSpecific())->describe($level); } + if ($isList) { + $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed(); + $innerType = ''; + if ($valueType !== null && !$isMixedValueType) { + $innerType = sprintf('<%s>', $valueType->describe($level)); + } + + $typeNames[] = 'list' . $innerType; + } + + usort($typeNames, static function ($a, $b) { + $cmp = strcasecmp($a, $b); + if ($cmp !== 0) { + return $cmp; + } + + return $a <=> $b; + }); + return implode('&', $typeNames); }, fn (): string => $this->describeItself($level, true), @@ -323,12 +353,25 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) $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; } @@ -350,10 +393,45 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) $skipTypeNames[] = 'string'; continue; } - if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) { - $typesToDescribe[$i] = $type; - $skipTypeNames[] = 'array'; - continue; + if ($isList || $isArray) { + if ($type instanceof ArrayType) { + $keyType = $type->getKeyType(); + $valueType = $type->getItemType(); + if ($isList) { + $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed(); + $valueTypeDescription = ''; + if (!$isMixedValueType) { + $valueTypeDescription = sprintf('<%s>', $valueType->describe($level)); + } + + $describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-list' : 'list') . $valueTypeDescription; + } else { + $isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed(); + $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed(); + $typeDescription = ''; + if (!$isMixedKeyType) { + $typeDescription = sprintf('<%s, %s>', $keyType->describe($level), $valueType->describe($level)); + } elseif (!$isMixedValueType) { + $typeDescription = sprintf('<%s>', $valueType->describe($level)); + } + + $describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-array' : 'array') . $typeDescription; + } + continue; + } elseif ($type instanceof ConstantArrayType) { + $description = $type->describe($level); + $descriptionWithoutKind = substr($description, strlen('array')); + $begin = $isList ? 'list' : 'array'; + if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) { + $begin = 'non-empty-' . $begin; + } + + $describedTypes[$i] = $begin . $descriptionWithoutKind; + continue; + } + if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) { + continue; + } } if ($type instanceof CallableType && $type->isCommonCallable()) { @@ -375,7 +453,6 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) $typesToDescribe[$i] = $type; } - $describedTypes = []; foreach ($baseTypes as $i => $type) { $typeDescription = $type->describe($level); @@ -389,36 +466,6 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) } } - if ( - str_starts_with($typeDescription, 'array<') - && in_array('array', $skipTypeNames, true) - ) { - $nonEmpty = false; - $typeName = 'array'; - foreach ($typesToDescribe as $j => $typeToDescribe) { - if ( - $typeToDescribe instanceof AccessoryArrayListType - && substr($typeDescription, 0, strlen('array, ')) === 'array, ' - ) { - $typeName = 'list'; - $typeDescription = 'array<' . substr($typeDescription, strlen('array, ')); - } elseif ($typeToDescribe instanceof NonEmptyArrayType) { - $nonEmpty = true; - } else { - continue; - } - - unset($typesToDescribe[$j]); - } - - if ($nonEmpty) { - $typeName = 'non-empty-' . $typeName; - } - - $describedTypes[$i] = $typeName . '<' . substr($typeDescription, strlen('array<')); - continue; - } - if (in_array($typeDescription, $skipTypeNames, true)) { continue; } @@ -460,7 +507,7 @@ public function hasProperty(string $propertyName): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName)); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } @@ -536,7 +583,7 @@ public function hasConstant(string $constantName): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName)); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { foreach ($this->types as $type) { if ($type->hasConstant($constantName)->yes()) { @@ -554,7 +601,10 @@ public function isIterable(): TrinaryLogic public function isIterableAtLeastOnce(): TrinaryLogic { - return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce()); + return $this->intersectResults( + static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce(), + static fn (Type $type): bool => !$type->isIterable()->no(), + ); } public function getArraySize(): Type @@ -624,6 +674,9 @@ public function isNumericString(): TrinaryLogic public function isNonEmptyString(): TrinaryLogic { + if ($this->isCallable()->yes() && $this->isString()->yes()) { + return TrinaryLogic::createYes(); + } return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString()); } @@ -637,9 +690,19 @@ public function isLiteralString(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString()); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic { - return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isClassStringType()); + 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 @@ -664,7 +727,9 @@ public function isScalar(): TrinaryLogic public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { - return new BooleanType(); + return $this->intersectResults( + static fn (Type $innerType): TrinaryLogic => $innerType->looseCompare($type, $phpVersion)->toTrinaryLogic() + )->toBooleanType(); } public function isOffsetAccessible(): TrinaryLogic @@ -684,6 +749,21 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic 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)); @@ -701,7 +781,64 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); + if ($this->isOversizedArray()->yes()) { + return $this->intersectTypes(static function (Type $type) use ($offsetType, $valueType, $unionValues): Type { + // avoid new HasOffsetValueType being intersected with oversized array + if (!$type instanceof ArrayType) { + return $type->setOffsetValueType($offsetType, $valueType, $unionValues); + } + + if (!$offsetType instanceof ConstantStringType && !$offsetType instanceof ConstantIntegerType) { + return $type->setOffsetValueType($offsetType, $valueType, $unionValues); + } + + if (!$offsetType->isSuperTypeOf($type->getKeyType())->yes()) { + return $type->setOffsetValueType($offsetType, $valueType, $unionValues); + } + + return TypeCombinator::intersect( + new ArrayType( + TypeCombinator::union($type->getKeyType(), $offsetType), + TypeCombinator::union($type->getItemType(), $valueType), + ), + new NonEmptyArrayType(), + ); + }); + } + + $result = $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); + + if ( + $offsetType !== null + && $this->isList()->yes() + && !$result->isList()->yes() + ) { + if ($this->isIterableAtLeastOnce()->yes() && (new ConstantIntegerType(1))->isSuperTypeOf($offsetType)->yes()) { + $result = TypeCombinator::intersect($result, new AccessoryArrayListType()); + } else { + foreach ($this->types as $type) { + if (!$type instanceof HasOffsetValueType && !$type instanceof HasOffsetType) { + continue; + } + + foreach ($type->getOffsetType()->getConstantScalarValues() as $constantScalarValue) { + if (!is_int($constantScalarValue)) { + continue; + } + if (IntegerRangeType::fromInterval(0, $constantScalarValue + 1)->isSuperTypeOf($offsetType)->yes()) { + $result = TypeCombinator::intersect($result, new AccessoryArrayListType()); + break 2; + } + } + } + } + } + + if ($this->isList()->yes() && $this->getIterableValueType()->isArray()->yes()) { + $result = TypeCombinator::intersect($result, new AccessoryArrayListType()); + } + + return $result; } public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type @@ -714,6 +851,11 @@ 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()); @@ -724,6 +866,11 @@ 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)); @@ -744,9 +891,14 @@ public function popArray(): Type return $this->intersectTypes(static fn (Type $type): Type => $type->popArray()); } - public function searchArray(Type $needleType): Type + 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)); + return $this->intersectTypes(static fn (Type $type): Type => $type->searchArray($needleType, $strict)); } public function shiftArray(): Type @@ -759,13 +911,34 @@ 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[md5($enumCase->describe(VerbosityLevel::typeOnly()))] = $enumCase; + $oneType[$enumCase->getClassName() . '::' . $enumCase->getEnumCaseName()] = $enumCase; } $compare[] = $oneType; } @@ -792,14 +965,14 @@ public function isCloneable(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCloneable()); } - public function isSmallerThan(Type $otherType): TrinaryLogic + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { - return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType)); + return $this->intersectResults(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->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType)); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType, $phpVersion)); } public function isNull(): TrinaryLogic @@ -866,34 +1039,34 @@ public function isInteger(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isInteger()); } - public function isGreaterThan(Type $otherType): TrinaryLogic + public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { - return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type)); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type, $phpVersion)); } - public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic + public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { - return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type)); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type, $phpVersion)); } - public function getSmallerType(): Type + public function getSmallerType(PhpVersion $phpVersion): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerType()); + return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerType($phpVersion)); } - public function getSmallerOrEqualType(): Type + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType()); + return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType($phpVersion)); } - public function getGreaterType(): Type + public function getGreaterType(PhpVersion $phpVersion): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterType()); + return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterType($phpVersion)); } - public function getGreaterOrEqualType(): Type + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType()); + return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType($phpVersion)); } public function toBoolean(): BooleanType @@ -952,7 +1125,10 @@ public function toArray(): Type public function toArrayKey(): Type { if ($this->isNumericString()->yes()) { - return new IntegerType(); + return TypeCombinator::union( + new IntegerType(), + $this, + ); } if ($this->isString()->yes()) { @@ -962,6 +1138,11 @@ public function toArrayKey(): Type return $this->intersectTypes(static fn (Type $type): Type => $type->toArrayKey()); } + 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(); @@ -1065,20 +1246,24 @@ public function getFiniteTypes(): array return $result; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self($properties['types']); - } - /** * @param callable(Type $type): TrinaryLogic $getResult + * @param (callable(Type $type): bool)|null $filter */ - private function intersectResults(callable $getResult): TrinaryLogic - { - return TrinaryLogic::lazyMaxMin($this->types, $getResult); + private function intersectResults( + callable $getResult, + ?callable $filter = null, + ): TrinaryLogic + { + $types = $this->types; + if ($filter !== null) { + $types = array_filter($types, $filter); + } + if (count($types) === 0) { + return TrinaryLogic::createNo(); + } + + return TrinaryLogic::lazyMaxMin($types, $getResult); } /** @@ -1098,12 +1283,18 @@ public function toPhpDocNode(): TypeNode $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; @@ -1126,11 +1317,70 @@ public function toPhpDocNode(): TypeNode $skipTypeNames[] = 'string'; continue; } - if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) { - $typesToDescribe[$i] = $type; - $skipTypeNames[] = 'array'; - continue; + + if ($isList || $isArray) { + if ($type instanceof ArrayType) { + $keyType = $type->getKeyType(); + $valueType = $type->getItemType(); + if ($isList) { + $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed(); + $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; @@ -1144,7 +1394,6 @@ public function toPhpDocNode(): TypeNode $typesToDescribe[$i] = $type; } - $describedTypes = []; foreach ($baseTypes as $i => $type) { $typeNode = $type->toPhpDocNode(); if ($typeNode instanceof GenericTypeNode && $typeNode->type->name === 'array') { 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 f36cab2e7f..c2b44b809b 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -20,6 +20,7 @@ use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use Traversable; use function array_merge; +use function get_class; use function sprintf; /** @api */ @@ -52,9 +53,6 @@ public function getItemType(): Type return $this->itemType; } - /** - * @return string[] - */ public function getReferencedClasses(): array { return array_merge( @@ -78,47 +76,42 @@ public function getConstantStrings(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type->isConstantArray()->yes() && $type->isIterableAtLeastOnce()->no()) { return AcceptsResult::createYes(); } if ($type->isIterable()->yes()) { - return $this->getIterableValueType()->acceptsWithReason($type->getIterableValueType(), $strictTypes) - ->and($this->getIterableKeyType()->acceptsWithReason($type->getIterableKeyType(), $strictTypes)); + return $this->getIterableValueType()->accepts($type->getIterableValueType(), $strictTypes) + ->and($this->getIterableKeyType()->accepts($type->getIterableKeyType(), $strictTypes)); } if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return AcceptsResult::createNo(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return $type->isIterable() + 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); @@ -130,16 +123,16 @@ private function isNestedTypeSuperTypeOf(Type $a, Type $b): TrinaryLogic if ($a->isExplicitMixed()) { if ($b->isExplicitMixed()) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof IntersectionType || $otherType instanceof UnionType) { return $otherType->isSuperTypeOf(new UnionType([ @@ -152,35 +145,30 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic } if ($otherType instanceof self) { - $limit = TrinaryLogic::createYes(); + $limit = IsSuperTypeOfResult::createYes(); } else { - $limit = TrinaryLogic::createMaybe(); + $limit = IsSuperTypeOfResult::createMaybe(); } if ($otherType->isConstantArray()->yes() && $otherType->isIterableAtLeastOnce()->no()) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } return $limit->and( - $otherType->isIterable(), + new IsSuperTypeOfResult($otherType->isIterable(), []), $otherType->getIterableValueType()->isSuperTypeOf($this->itemType), $otherType->getIterableKeyType()->isSuperTypeOf($this->keyType), ); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; - } - - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult - { - return new AcceptsResult($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; } @@ -247,9 +235,25 @@ public function toArrayKey(): Type return new ErrorType(); } - public function isOffsetAccessLegal(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); + 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 @@ -372,7 +376,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -512,12 +526,4 @@ public function toPhpDocNode(): TypeNode ); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self($properties['keyType'], $properties['itemType']); - } - } diff --git a/src/Type/JustNullableTypeTrait.php b/src/Type/JustNullableTypeTrait.php index 294b7d1d2b..5435c540ff 100644 --- a/src/Type/JustNullableTypeTrait.php +++ b/src/Type/JustNullableTypeTrait.php @@ -8,9 +8,6 @@ trait JustNullableTypeTrait { - /** - * @return string[] - */ public function getReferencedClasses(): array { return []; @@ -26,35 +23,30 @@ public function getObjectClassReflections(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof static) { return AcceptsResult::createYes(); } if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } 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 @@ -147,7 +139,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic { return TrinaryLogic::createNo(); } diff --git a/src/Type/KeyOfType.php b/src/Type/KeyOfType.php index 32eab8b019..10a4cb2ea5 100644 --- a/src/Type/KeyOfType.php +++ b/src/Type/KeyOfType.php @@ -91,14 +91,4 @@ public function toPhpDocNode(): TypeNode return new GenericTypeNode(new IdentifierTypeNode('key-of'), [$this->type->toPhpDocNode()]); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self( - $properties['type'], - ); - } - } diff --git a/src/Type/LazyTypeAliasResolverProvider.php b/src/Type/LazyTypeAliasResolverProvider.php index d270e691f2..28af91a22d 100644 --- a/src/Type/LazyTypeAliasResolverProvider.php +++ b/src/Type/LazyTypeAliasResolverProvider.php @@ -2,9 +2,11 @@ namespace PHPStan\Type; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; -class LazyTypeAliasResolverProvider implements TypeAliasResolverProvider +#[AutowiredService(as: TypeAliasResolverProvider::class)] +final class LazyTypeAliasResolverProvider implements TypeAliasResolverProvider { public function __construct(private Container $container) diff --git a/src/Type/MethodTypeSpecifyingExtension.php b/src/Type/MethodTypeSpecifyingExtension.php index 9e56ea330f..f0f11de2f4 100644 --- a/src/Type/MethodTypeSpecifyingExtension.php +++ b/src/Type/MethodTypeSpecifyingExtension.php @@ -28,6 +28,7 @@ interface MethodTypeSpecifyingExtension { + /** @return class-string */ public function getClass(): string; public function isMethodSupported(MethodReflection $methodReflection, MethodCall $node, TypeSpecifierContext $context): bool; diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 402d192a7d..1245743947 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -6,13 +6,13 @@ 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\Dummy\DummyConstantReflection; +use PHPStan\Reflection\Dummy\DummyClassConstantReflection; use PHPStan\Reflection\Dummy\DummyMethodReflection; use PHPStan\Reflection\Dummy\DummyPropertyReflection; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection; @@ -21,17 +21,23 @@ 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\OversizedArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use function get_class; use function sprintf; /** @api */ @@ -57,9 +63,6 @@ public function __construct( $this->subtractedType = $subtractedType; } - /** - * @return string[] - */ public function getReferencedClasses(): array { return []; @@ -90,67 +93,73 @@ public function getConstantStrings(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { return AcceptsResult::createYes(); } - public function isSuperTypeOfMixed(MixedType $type): TrinaryLogic + public function isSuperTypeOfMixed(MixedType $type): IsSuperTypeOfResult { if ($this->subtractedType === null) { if ($this->isExplicitMixed) { if ($type->isExplicitMixed) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type->subtractedType === null) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } $isSuperType = $type->subtractedType->isSuperTypeOf($this->subtractedType); if ($isSuperType->yes()) { if ($this->isExplicitMixed) { if ($type->isExplicitMixed) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($this->subtractedType === null || $type instanceof NeverType) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type instanceof self) { if ($type->subtractedType === null) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } $isSuperType = $type->subtractedType->isSuperTypeOf($this->subtractedType); if ($isSuperType->yes()) { - return TrinaryLogic::createYes(); + return $isSuperType; } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); + } + + $result = $this->subtractedType->isSuperTypeOf($type)->negate(); + if ($result->no()) { + return IsSuperTypeOfResult::createNo([ + sprintf( + 'Type %s has already been eliminated from %s.', + $this->subtractedType->describe(VerbosityLevel::precise()), + $this->describe(VerbosityLevel::typeOnly()), + ), + ]); } - return $this->subtractedType->isSuperTypeOf($type)->negate(); + return $result; } public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type @@ -171,13 +180,18 @@ public function unsetOffset(Type $offsetType): Type 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 AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new UnionType([new IntegerType(), new StringType()]))); + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new UnionType([new IntegerType(), new StringType()])), new AccessoryArrayListType()); } public function getValuesArray(): Type @@ -186,7 +200,16 @@ public function getValuesArray(): Type return new ErrorType(); } - return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new MixedType($this->isExplicitMixed))); + 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 @@ -225,7 +248,16 @@ public function popArray(): Type return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); } - public function searchArray(Type $needleType): Type + 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(); @@ -249,7 +281,25 @@ public function shuffleArray(): Type return new ErrorType(); } - return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new MixedType($this->isExplicitMixed))); + 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 @@ -275,7 +325,7 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) public function equals(Type $type): bool { - if (!$type instanceof self) { + if (get_class($type) !== static::class) { return false; } @@ -294,30 +344,25 @@ 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(); - } - - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic - { - return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + return IsSuperTypeOfResult::createMaybe(); } - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - $isSuperType = new AcceptsResult($this->isSuperTypeOf($acceptingType), []); + $isSuperType = $this->isSuperTypeOf($acceptingType)->toAcceptsResult(); if ($isSuperType->no()) { return $isSuperType; } @@ -359,14 +404,14 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createYes(); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - $property = new DummyPropertyReflection(); + $property = new DummyPropertyReflection($propertyName); return new CallbackUnresolvedPropertyPrototypeReflection( $property, $property->getDeclaringClass(), @@ -411,9 +456,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 @@ -429,7 +474,9 @@ public function describe(VerbosityLevel $level): string function () use ($level): string { $description = 'mixed'; if ($this->subtractedType !== null) { - $description .= sprintf('~%s', $this->subtractedType->describe($level)); + $description .= $this->subtractedType instanceof UnionType + ? sprintf('~(%s)', $this->subtractedType->describe($level)) + : sprintf('~%s', $this->subtractedType->describe($level)); } return $description; @@ -437,7 +484,9 @@ function () use ($level): string { function () use ($level): string { $description = 'mixed'; if ($this->subtractedType !== null) { - $description .= sprintf('~%s', $this->subtractedType->describe($level)); + $description .= $this->subtractedType instanceof UnionType + ? sprintf('~(%s)', $this->subtractedType->describe($level)) + : sprintf('~%s', $this->subtractedType->describe($level)); } if ($this->isExplicitMixed) { @@ -453,8 +502,10 @@ function () use ($level): string { public function toBoolean(): BooleanType { - if ($this->subtractedType !== null && StaticTypeFactory::falsey()->equals($this->subtractedType)) { - return new ConstantBooleanType(true); + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(StaticTypeFactory::falsey())->yes()) { + return new ConstantBooleanType(true); + } } return new BooleanType(); @@ -462,10 +513,10 @@ public function toBoolean(): BooleanType public function toNumber(): Type { - return new UnionType([ + return TypeCombinator::union( $this->toInteger(), $this->toFloat(), - ]); + ); } public function toAbsoluteNumber(): Type @@ -475,6 +526,24 @@ public function toAbsoluteNumber(): Type 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(); } @@ -485,6 +554,32 @@ public function toFloat(): Type public function toString(): Type { + if ($this->subtractedType !== null) { + $castsToEmptyString = new UnionType([ + new NullType(), + new ConstantBooleanType(false), + new ConstantStringType(''), + ]); + if ($this->subtractedType->isSuperTypeOf($castsToEmptyString)->yes()) { + $accessories = [ + new StringType(), + new AccessoryNonEmptyStringType(), + ]; + + $castsToZeroString = new UnionType([ + new ConstantFloatType(0.0), + new ConstantStringType('0'), + new ConstantIntegerType(0), + ]); + if ($this->subtractedType->isSuperTypeOf($castsToZeroString)->yes()) { + $accessories[] = new AccessoryNonFalsyStringType(); + } + return new IntersectionType( + $accessories, + ); + } + } + return new StringType(); } @@ -497,7 +592,12 @@ public function toArray(): Type public function toArrayKey(): Type { - return new UnionType([new IntegerType(), new StringType()]); + return new BenevolentUnionType([new IntegerType(), new StringType()]); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; } public function isIterable(): TrinaryLogic @@ -845,7 +945,39 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $lowercaseString = TypeCombinator::intersect( + new StringType(), + new AccessoryLowercaseStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($lowercaseString)->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $uppercaseString = TypeCombinator::intersect( + new StringType(), + new AccessoryUppercaseStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($uppercaseString)->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic { if ($this->subtractedType !== null) { if ($this->subtractedType->isSuperTypeOf(new StringType())->yes()) { @@ -861,7 +993,7 @@ public function isClassStringType(): TrinaryLogic public function getClassStringObjectType(): Type { - if (!$this->isClassStringType()->no()) { + if (!$this->isClassString()->no()) { return new ObjectWithoutClassType(); } @@ -935,15 +1067,4 @@ public function toPhpDocNode(): TypeNode return new IdentifierTypeNode('mixed'); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self( - $properties['isExplicitMixed'], - $properties['subtractedType'] ?? null, - ); - } - } diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index b619768b6d..659368d6da 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -5,10 +5,10 @@ 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\ExtendedMethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; @@ -39,9 +39,6 @@ public function isExplicit(): bool return $this->isExplicit; } - /** - * @return string[] - */ public function getReferencedClasses(): array { return []; @@ -72,23 +69,18 @@ public function getConstantStrings(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { return AcceptsResult::createYes(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool @@ -96,19 +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->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; - } - - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult - { - return new AcceptsResult($this->isSubTypeOf($acceptingType), []); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function describe(VerbosityLevel $level): string @@ -141,7 +128,7 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { throw new ShouldNotHappenException(); } @@ -181,7 +168,7 @@ public function hasConstant(string $constantName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { throw new ShouldNotHappenException(); } @@ -286,6 +273,11 @@ 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(); @@ -296,6 +288,11 @@ 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(); @@ -316,7 +313,12 @@ public function popArray(): Type return new NeverType(); } - public function searchArray(Type $needleType): Type + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return new NeverType(); + } + + public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type { return new NeverType(); } @@ -331,6 +333,16 @@ 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::createNo(); @@ -381,6 +393,11 @@ public function toArrayKey(): Type return $this; } + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + public function traverse(callable $cb): Type { return $this; @@ -466,7 +483,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -516,12 +543,4 @@ public function toPhpDocNode(): TypeNode return new IdentifierTypeNode('never'); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self($properties['isExplicit']); - } - } diff --git a/src/Type/NewObjectType.php b/src/Type/NewObjectType.php index 57b932a398..93d14c6936 100644 --- a/src/Type/NewObjectType.php +++ b/src/Type/NewObjectType.php @@ -91,14 +91,4 @@ public function toPhpDocNode(): TypeNode return new GenericTypeNode(new IdentifierTypeNode('new'), [$this->type->toPhpDocNode()]); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self( - $properties['type'], - ); - } - } diff --git a/src/Type/NonAcceptingNeverType.php b/src/Type/NonAcceptingNeverType.php index 3eaddf53cb..dd14d3f9d2 100644 --- a/src/Type/NonAcceptingNeverType.php +++ b/src/Type/NonAcceptingNeverType.php @@ -2,8 +2,6 @@ namespace PHPStan\Type; -use PHPStan\TrinaryLogic; - /** @api */ class NonAcceptingNeverType extends NeverType { @@ -14,19 +12,19 @@ public function __construct() parent::__construct(true); } - 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 parent) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof NeverType) { return AcceptsResult::createYes(); diff --git a/src/Type/NonexistentParentClassType.php b/src/Type/NonexistentParentClassType.php index bbf9dceec8..0b91e093e6 100644 --- a/src/Type/NonexistentParentClassType.php +++ b/src/Type/NonexistentParentClassType.php @@ -5,10 +5,10 @@ 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\ExtendedMethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; @@ -67,7 +67,7 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { throw new ShouldNotHappenException(); } @@ -107,7 +107,7 @@ public function hasConstant(string $constantName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { throw new ShouldNotHappenException(); } @@ -157,6 +157,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function toCoercedArgumentType(bool $strictTypes): Type + { + return new ErrorType(); + } + public function isOffsetAccessLegal(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -192,12 +197,4 @@ public function toPhpDocNode(): TypeNode return new IdentifierTypeNode('parent'); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self(); - } - } diff --git a/src/Type/NullType.php b/src/Type/NullType.php index e72cf25be2..ef2408e713 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -36,9 +36,6 @@ public function __construct() { } - /** - * @return string[] - */ public function getReferencedClasses(): array { return []; @@ -72,35 +69,30 @@ public function generalize(GeneralizePrecision $precision): Type return $this; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof self) { return AcceptsResult::createYes(); } if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } 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 @@ -108,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(); @@ -174,6 +174,11 @@ public function toArrayKey(): Type return new ConstantStringType(''); } + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + public function isOffsetAccessible(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -295,7 +300,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -331,15 +346,19 @@ public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType return new ConstantBooleanType($this->getValue() == []); // phpcs:ignore } + if ($type instanceof CompoundType) { + return $type->looseCompare($this, $phpVersion); + } + return new BooleanType(); } - public function getSmallerType(): Type + 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([ @@ -352,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), @@ -365,7 +384,7 @@ public function getGreaterType(): Type ])); } - public function getGreaterOrEqualType(): Type + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { return new MixedType(); } @@ -390,12 +409,4 @@ public function toPhpDocNode(): TypeNode return new IdentifierTypeNode('null'); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self(); - } - } diff --git a/src/Type/ObjectShapePropertyReflection.php b/src/Type/ObjectShapePropertyReflection.php index a79f924417..1d022cb726 100644 --- a/src/Type/ObjectShapePropertyReflection.php +++ b/src/Type/ObjectShapePropertyReflection.php @@ -3,18 +3,25 @@ namespace PHPStan\Type; 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 stdClass; -class ObjectShapePropertyReflection implements PropertyReflection +final class ObjectShapePropertyReflection implements ExtendedPropertyReflection { - public function __construct(private Type $type) + public function __construct(private string $name, private Type $type) { } + public function getName(): string + { + return $this->name; + } + public function getDeclaringClass(): ClassReflection { $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); @@ -42,6 +49,26 @@ 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; @@ -82,4 +109,49 @@ 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 []; + } + } diff --git a/src/Type/ObjectShapeType.php b/src/Type/ObjectShapeType.php index a00324259a..668eac366f 100644 --- a/src/Type/ObjectShapeType.php +++ b/src/Type/ObjectShapeType.php @@ -3,7 +3,6 @@ namespace PHPStan\Type; use PHPStan\Analyser\OutOfClassScope; -use PHPStan\Broker\Broker; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; @@ -11,9 +10,9 @@ use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\MissingPropertyFromReflectionException; use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; @@ -103,7 +102,7 @@ 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(); } @@ -114,7 +113,7 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember throw new ShouldNotHappenException(); } - $property = new ObjectShapePropertyReflection($this->properties[$propertyName]); + $property = new ObjectShapePropertyReflection($propertyName, $this->properties[$propertyName]); return new CallbackUnresolvedPropertyPrototypeReflection( $property, $property->getDeclaringClass(), @@ -123,22 +122,16 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember ); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); foreach ($type->getObjectClassReflections() as $classReflection) { if (!UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( $reflectionProvider, - Broker::getInstance()->getUniversalObjectCratesClasses(), $classReflection, )) { continue; @@ -209,7 +202,7 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult $otherPropertyType = $otherProperty->getReadableType(); $verbosity = VerbosityLevel::getRecommendedLevelByType($propertyType, $otherPropertyType); - $acceptsValue = $propertyType->acceptsWithReason($otherPropertyType, $strictTypes)->decorateReasons( + $acceptsValue = $propertyType->accepts($otherPropertyType, $strictTypes)->decorateReasons( static fn (string $reason) => sprintf( 'Property ($%s) type %s does not accept type %s: %s', $propertyName, @@ -237,33 +230,32 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult return $result->and(new AcceptsResult($type->isObject(), [])); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } if ($type instanceof ObjectWithoutClassType) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); foreach ($type->getObjectClassReflections() as $classReflection) { if (!UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( $reflectionProvider, - Broker::getInstance()->getUniversalObjectCratesClasses(), $classReflection, )) { continue; } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - $result = TrinaryLogic::createYes(); + $result = IsSuperTypeOfResult::createYes(); $scope = new OutOfClassScope(); foreach ($this->properties as $propertyName => $propertyType) { - $hasProperty = $type->hasProperty($propertyName); + $hasProperty = new IsSuperTypeOfResult($type->hasProperty($propertyName), []); if ($hasProperty->no()) { if (in_array($propertyName, $this->optionalProperties, true)) { continue; @@ -271,7 +263,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return $hasProperty; } if ($hasProperty->maybe() && in_array($propertyName, $this->optionalProperties, true)) { - $hasProperty = TrinaryLogic::createYes(); + $hasProperty = IsSuperTypeOfResult::createYes(); } $result = $result->and($hasProperty); @@ -283,15 +275,15 @@ public function isSuperTypeOf(Type $type): TrinaryLogic } if (!$otherProperty->isPublic()) { - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } if ($otherProperty->isStatic()) { - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } if (!$otherProperty->isReadable()) { - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } $otherPropertyType = $otherProperty->getReadableType(); @@ -302,7 +294,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic $result = $result->and($isSuperType); } - return $result->and($type->isObject()); + return $result->and(new IsSuperTypeOfResult($type->isObject(), [])); } public function equals(Type $type): bool @@ -428,11 +420,6 @@ public function describe(VerbosityLevel $level): string ); } - public function isOffsetAccessLegal(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - public function getEnumCases(): array { return []; @@ -530,12 +517,4 @@ public function toPhpDocNode(): TypeNode return new ObjectShapeNode($items); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self($properties['properties'], $properties['optionalProperties']); - } - } diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 45a04563c9..78e4473410 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -3,32 +3,30 @@ namespace PHPStan\Type; use ArrayAccess; +use ArrayObject; use Closure; use Countable; -use DateTime; -use DateTimeImmutable; use DateTimeInterface; -use Error; -use Exception; 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\Dummy\DummyPropertyReflection; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +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; @@ -36,6 +34,8 @@ use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; @@ -47,6 +47,7 @@ use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +use Stringable; use Throwable; use Traversable; use function array_key_exists; @@ -68,11 +69,23 @@ class ObjectType implements TypeWithClassName, SubtractableType use UndecidedComparisonTypeTrait; use NonGeneralizableTypeTrait; - private const EXTRA_OFFSET_CLASSES = ['SimpleXMLElement', 'DOMNodeList', 'Threaded']; + private const EXTRA_OFFSET_CLASSES = [ + 'DOMNamedNodeMap', // Only read and existence + 'Dom\NamedNodeMap', // Only read and existence + 'DOMNodeList', // Only read and existence + 'Dom\NodeList', // Only read and existence + 'Dom\HTMLCollection', // Only read and existence + 'Dom\DtdNamedNodeMap', // Only read and existence + 'PDORow', // Only read and existence + 'ResourceBundle', // Only read + 'FFI\CData', // Very funky and weird + 'SimpleXMLElement', + 'Threaded', + ]; private ?Type $subtractedType; - /** @var array> */ + /** @var array> */ private static array $superTypes = []; private ?self $cachedParent = null; @@ -120,21 +133,6 @@ public static function resetCaches(): void self::$enumCases = []; } - private static function createFromReflection(ClassReflection $reflection): self - { - if (!$reflection->isGeneric()) { - return new ObjectType($reflection->getName()); - } - - return new GenericObjectType( - $reflection->getName(), - $reflection->typeMapToList($reflection->getActiveTemplateTypeMap()), - null, - null, - $reflection->varianceMapToList($reflection->getCallSiteVarianceMap()), - ); - } - public function getClassName(): string { return $this->className; @@ -147,7 +145,8 @@ 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(); } @@ -162,7 +161,7 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } @@ -213,7 +212,17 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember 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; @@ -235,6 +244,9 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember ); } + /** + * @deprecated Not in use anymore. + */ public function getPropertyWithoutTransformingStatic(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection { $classReflection = $this->getNakedClassReflection(); @@ -253,9 +265,6 @@ public function getPropertyWithoutTransformingStatic(string $propertyName, Class return $classReflection->getProperty($propertyName, $scope); } - /** - * @return string[] - */ public function getReferencedClasses(): array { return [$this->className]; @@ -279,19 +288,14 @@ public function getObjectClassReflections(): array return [$classReflection]; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof StaticType) { return $this->checkSubclassAcceptability($type->getClassName()); } if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } if ($type instanceof ClosureType) { @@ -314,11 +318,11 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult 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 TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } $thisDescription = $this->describeCache(); @@ -338,27 +342,27 @@ public function isSuperTypeOf(Type $type): TrinaryLogic } if ($type instanceof ClosureType) { - return self::$superTypes[$thisDescription][$description] = $this->isInstanceOf(Closure::class); + 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(); + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } - $transformResult = static fn (TrinaryLogic $result) => $result; + $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 (TrinaryLogic $result) => $result->and(TrinaryLogic::createMaybe()); + $transformResult = static fn (IsSuperTypeOfResult $result) => $result->and(IsSuperTypeOfResult::createMaybe()); } } @@ -368,7 +372,7 @@ 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(); } } @@ -377,44 +381,60 @@ public function isSuperTypeOf(Type $type): TrinaryLogic throw new ShouldNotHappenException(); } - if ($thatClassNames[0] === $thisClassName) { - return $transformResult(TrinaryLogic::createYes()); - } - - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); $thisClassReflection = $this->getClassReflection(); + $thatClassReflections = $type->getObjectClassReflections(); + if (count($thatClassReflections) === 1) { + $thatClassReflection = $thatClassReflections[0]; + } else { + $thatClassReflection = null; + } - if ($thisClassReflection === null || !$reflectionProvider->hasClass($thatClassNames[0])) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); + if ($thisClassReflection === null || $thatClassReflection === null) { + if ($thatClassNames[0] === $thisClassName) { + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); + } + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } - $thatClassReflection = $reflectionProvider->getClass($thatClassNames[0]); + 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()); + } + } + + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); + } if ($thisClassReflection->isTrait() || $thatClassReflection->isTrait()) { - return TrinaryLogic::createNo(); + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createNo(); } if ($thisClassReflection->getName() === $thatClassReflection->getName()) { - return self::$superTypes[$thisDescription][$description] = $transformResult(TrinaryLogic::createYes()); + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); } - if ($thatClassReflection->isSubclassOf($thisClassName)) { - return self::$superTypes[$thisDescription][$description] = $transformResult(TrinaryLogic::createYes()); + if ($thatClassReflection->isSubclassOfClass($thisClassReflection)) { + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); } - if ($thisClassReflection->isSubclassOf($thatClassNames[0])) { - 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 @@ -469,7 +489,7 @@ private function checkSubclassAcceptability(string $thatClass): AcceptsResult } return AcceptsResult::createFromBoolean( - $thatReflection->isSubclassOf($thisReflection->getName()), + $thatReflection->isSubclassOfClass($thisReflection), ); } @@ -487,7 +507,9 @@ public function describe(VerbosityLevel $level): string $preciseWithSubtracted = function () use ($level): string { $description = $this->className; if ($this->subtractedType !== null) { - $description .= sprintf('~%s', $this->subtractedType->describe($level)); + $description .= $this->subtractedType instanceof UnionType + ? sprintf('~(%s)', $this->subtractedType->describe($level)) + : sprintf('~%s', $this->subtractedType->describe($level)); } return $description; @@ -538,7 +560,9 @@ private function describeCache(): string } if ($this->subtractedType !== null) { - $description .= sprintf('~%s', $this->subtractedType->describe(VerbosityLevel::cache())); + $description .= $this->subtractedType instanceof UnionType + ? sprintf('~(%s)', $this->subtractedType->describe(VerbosityLevel::cache())) + : sprintf('~%s', $this->subtractedType->describe(VerbosityLevel::cache())); } $reflection = $this->classReflection; @@ -546,6 +570,10 @@ private function describeCache(): string $description .= '-'; $description .= (string) $reflection->getNativeReflection()->getStartLine(); $description .= '-'; + + if ($reflection->hasFinalByKeywordOverride()) { + $description .= 'f=' . ($reflection->isFinalByKeyword() ? 't' : 'f'); + } } return $this->cachedDescription = $description; @@ -591,13 +619,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(); @@ -614,9 +650,9 @@ public function toArray(): Type if ( !$classReflection->getNativeReflection()->isUserDefined() + || $classReflection->is(ArrayObject::class) || UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( $reflectionProvider, - Broker::getInstance()->getUniversalObjectCratesClasses(), $classReflection, ) ) { @@ -669,9 +705,29 @@ public function toArrayKey(): Type return $this->toString(); } + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + $classReflection = $this->getClassReflection(); + if ( + $classReflection === null + || !$classReflection->hasNativeMethod('__toString') + ) { + return $this; + } + + return TypeCombinator::union($this, $this->toString()); + } + + return $this; + } + public function toBoolean(): BooleanType { - if ($this->isInstanceOf('SimpleXMLElement')->yes()) { + if ( + $this->isInstanceOf('SimpleXMLElement')->yes() + || $this->isInstanceOf('BcMath\Number')->yes() + ) { return new BooleanType(); } @@ -690,7 +746,23 @@ public function isEnum(): TrinaryLogic return TrinaryLogic::createMaybe(); } - return TrinaryLogic::createFromBoolean($classReflection->isEnum()); + 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 @@ -784,17 +856,23 @@ 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) { @@ -833,7 +911,7 @@ public function getTemplateType(string $ancestorClassName, string $templateTypeN return new MixedType(false); } - return $bound; + return TemplateTypeHelper::resolveToDefaults($templateType); } return $type; @@ -861,16 +939,18 @@ public function getArraySize(): Type return new ErrorType(); } - return IntegerRangeType::fromInterval(0, null); + if ($this->hasMethod('count')->yes() === false) { + return IntegerRangeType::fromInterval(0, null); + } + + 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 => ParametersAcceptorSelector::selectSingle( - $this->getMethod('getIterator', new OutOfClassScope())->getVariants(), - )->getReturnType()->getIterableKeyType()); + $keyType = RecursionGuard::run($this, fn (): Type => $this->getMethod('getIterator', new OutOfClassScope())->getOnlyVariant()->getReturnType()->getIterableKeyType()); $isTraversable = true; if (!$keyType instanceof MixedType || $keyType->isExplicitMixed()) { return $keyType; @@ -889,9 +969,7 @@ public function getIterableKeyType(): Type } if ($this->isInstanceOf(Iterator::class)->yes()) { - return RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle( - $this->getMethod('key', new OutOfClassScope())->getVariants(), - )->getReturnType()); + return RecursionGuard::run($this, fn (): Type => $this->getMethod('key', new OutOfClassScope())->getOnlyVariant()->getReturnType()); } if ($extraOffsetAccessible) { @@ -919,9 +997,7 @@ public function getIterableValueType(): Type { $isTraversable = false; if ($this->isInstanceOf(IteratorAggregate::class)->yes()) { - $valueType = RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle( - $this->getMethod('getIterator', new OutOfClassScope())->getVariants(), - )->getReturnType()->getIterableValueType()); + $valueType = RecursionGuard::run($this, fn (): Type => $this->getMethod('getIterator', new OutOfClassScope())->getOnlyVariant()->getReturnType()->getIterableValueType()); $isTraversable = true; if (!$valueType instanceof MixedType || $valueType->isExplicitMixed()) { return $valueType; @@ -940,9 +1016,7 @@ public function getIterableValueType(): Type } if ($this->isInstanceOf(Iterator::class)->yes()) { - return RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle( - $this->getMethod('current', new OutOfClassScope())->getVariants(), - )->getReturnType()); + return RecursionGuard::run($this, fn (): Type => $this->getMethod('current', new OutOfClassScope())->getOnlyVariant()->getReturnType()); } if ($extraOffsetAccessible) { @@ -1041,7 +1115,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -1085,10 +1169,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(); } } @@ -1120,7 +1201,7 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic { if ($this->isInstanceOf(ArrayAccess::class)->yes()) { $acceptedOffsetType = RecursionGuard::run($this, function (): Type { - $parameters = ParametersAcceptorSelector::selectSingle($this->getMethod('offsetSet', new OutOfClassScope())->getVariants())->getParameters(); + $parameters = $this->getMethod('offsetSet', new OutOfClassScope())->getOnlyVariant()->getParameters(); if (count($parameters) < 2) { throw new ShouldNotHappenException(sprintf( 'Method %s::%s() has less than 2 parameters.', @@ -1147,12 +1228,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, fn (): Type => ParametersAcceptorSelector::selectSingle($this->getMethod('offsetGet', new OutOfClassScope())->getVariants())->getReturnType()); + if (!$this->isExtraOffsetAccessibleClass()->no()) { + return new MixedType(); } return new ErrorType(); @@ -1167,7 +1248,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni if ($this->isInstanceOf(ArrayAccess::class)->yes()) { $acceptedValueType = new NeverType(); $acceptedOffsetType = RecursionGuard::run($this, function () use (&$acceptedValueType): Type { - $parameters = ParametersAcceptorSelector::selectSingle($this->getMethod('offsetSet', new OutOfClassScope())->getVariants())->getParameters(); + $parameters = $this->getMethod('offsetSet', new OutOfClassScope())->getOnlyVariant()->getParameters(); if (count($parameters) < 2) { throw new ShouldNotHappenException(sprintf( 'Method %s::%s() has less than 2 parameters.', @@ -1309,7 +1390,7 @@ private function findCallableParametersAcceptors(): ?array ); } - if (!$classReflection->getNativeReflection()->isFinal()) { + if (!$classReflection->isFinalByKeyword()) { return [new TrivialParametersAcceptor()]; } @@ -1321,17 +1402,6 @@ public function isCloneable(): TrinaryLogic return TrinaryLogic::createYes(); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self( - $properties['className'], - $properties['subtractedType'] ?? null, - ); - } - public function isInstanceOf(string $className): TrinaryLogic { $classReflection = $this->getClassReflection(); @@ -1339,7 +1409,7 @@ public function isInstanceOf(string $className): TrinaryLogic return TrinaryLogic::createMaybe(); } - if ($classReflection->getName() === $className || $classReflection->isSubclassOf($className)) { + if ($classReflection->is($className)) { return TrinaryLogic::createYes(); } @@ -1487,10 +1557,7 @@ public function getClassReflection(): ?ClassReflection return $classReflection; } - /** - * @return self|null - */ - public function getAncestorWithClassName(string $className): ?TypeWithClassName + public function getAncestorWithClassName(string $className): ?self { if ($this->className === $className) { return $this; @@ -1559,7 +1626,7 @@ private function getParent(): ?ObjectType return null; } - return $this->cachedParent = self::createFromReflection($parentReflection); + return $this->cachedParent = $parentReflection->getObjectType(); } /** @return ObjectType[] */ @@ -1573,28 +1640,26 @@ private function getInterfaces(): array return $this->cachedInterfaces = []; } - return $this->cachedInterfaces = array_map(static fn (ClassReflection $interfaceReflection): self => 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 ($this->getClassName() === DateTimeInterface::class) { - if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === DateTimeImmutable::class) { - return new ObjectType(DateTime::class); - } - - if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === DateTime::class) { - return new ObjectType(DateTimeImmutable::class); - } - } + if ($typeToRemove instanceof ObjectType) { + foreach (UnionType::EQUAL_UNION_CLASSES as $baseClass => $classes) { + if ($this->getClassName() !== $baseClass) { + continue; + } - if ($this->getClassName() === Throwable::class) { - if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === Error::class) { - return new ObjectType(Exception::class); // phpcs:ignore SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly.ReferencedGeneralException - } + foreach ($classes as $index => $class) { + if ($typeToRemove->getClassName() === $class) { + unset($classes[$index]); - if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === Exception::class) { // phpcs:ignore SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly.ReferencedGeneralException - return new ObjectType(Error::class); + return TypeCombinator::union( + ...array_map(static fn (string $objectClass): Type => new ObjectType($objectClass), $classes), + ); + } + } } } diff --git a/src/Type/ObjectWithoutClassType.php b/src/Type/ObjectWithoutClassType.php index 2d0cc64c1e..a3b47c7686 100644 --- a/src/Type/ObjectWithoutClassType.php +++ b/src/Type/ObjectWithoutClassType.php @@ -4,7 +4,6 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -use PHPStan\TrinaryLogic; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\ObjectTypeTrait; @@ -34,9 +33,6 @@ public function __construct( $this->subtractedType = $subtractedType; } - /** - * @return string[] - */ public function getReferencedClasses(): array { return []; @@ -52,15 +48,10 @@ public function getObjectClassReflections(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return AcceptsResult::createFromBoolean( @@ -68,7 +59,7 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult ); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); @@ -76,28 +67,28 @@ 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 ObjectShapeType) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type->getObjectClassNames() === []) { - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } if ($this->subtractedType === null) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } return $this->subtractedType->isSuperTypeOf($type)->negate(); @@ -132,7 +123,9 @@ public function describe(VerbosityLevel $level): string function () use ($level): string { $description = 'object'; if ($this->subtractedType !== null) { - $description .= sprintf('~%s', $this->subtractedType->describe($level)); + $description .= $this->subtractedType instanceof UnionType + ? sprintf('~(%s)', $this->subtractedType->describe($level)) + : sprintf('~%s', $this->subtractedType->describe($level)); } return $description; @@ -140,11 +133,6 @@ function () use ($level): string { ); } - public function isOffsetAccessLegal(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - public function getEnumCases(): array { return []; @@ -228,12 +216,4 @@ public function toPhpDocNode(): TypeNode return new IdentifierTypeNode('object'); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self($properties['subtractedType'] ?? null); - } - } diff --git a/src/Type/OffsetAccessType.php b/src/Type/OffsetAccessType.php index 430d38332c..5e4ef1aec3 100644 --- a/src/Type/OffsetAccessType.php +++ b/src/Type/OffsetAccessType.php @@ -114,15 +114,4 @@ public function toPhpDocNode(): TypeNode ); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self( - $properties['type'], - $properties['offset'], - ); - } - } diff --git a/src/Type/OperatorTypeSpecifyingExtensionRegistry.php b/src/Type/OperatorTypeSpecifyingExtensionRegistry.php index 654d7a1ea8..7b84648f28 100644 --- a/src/Type/OperatorTypeSpecifyingExtensionRegistry.php +++ b/src/Type/OperatorTypeSpecifyingExtensionRegistry.php @@ -2,33 +2,19 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; -use PHPStan\Reflection\BrokerAwareExtension; use function array_filter; use function array_values; -class OperatorTypeSpecifyingExtensionRegistry +final class OperatorTypeSpecifyingExtensionRegistry { /** * @param OperatorTypeSpecifyingExtension[] $extensions */ public function __construct( - ?Broker $broker, private array $extensions, ) { - if ($broker === null) { - return; - } - - foreach ($extensions as $extension) { - if (!$extension instanceof BrokerAwareExtension) { - continue; - } - - $extension->setBroker($broker); - } } /** diff --git a/src/Type/PHPStan/ClassNameUsageLocationCreateIdentifierDynamicReturnTypeExtension.php b/src/Type/PHPStan/ClassNameUsageLocationCreateIdentifierDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..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 7299a08100..e616fffc0e 100644 --- a/src/Type/ParserNodeTypeToPHPStanType.php +++ b/src/Type/ParserNodeTypeToPHPStanType.php @@ -13,7 +13,7 @@ use function in_array; use function strtolower; -class ParserNodeTypeToPHPStanType +final class ParserNodeTypeToPHPStanType { /** diff --git a/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php b/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php index d47d7f39eb..7de2eebbd7 100644 --- a/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php @@ -4,12 +4,14 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; -class AbsFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class AbsFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php b/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php index c4ab9e8b9c..b039b62d00 100644 --- a/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; @@ -13,12 +14,12 @@ use PHPStan\Type\TypeCombinator; use function array_key_exists; -class ArgumentBasedFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArgumentBasedFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { private const FUNCTION_NAMES = [ 'array_unique' => 0, - 'array_change_key_case' => 0, 'array_diff_assoc' => 0, 'array_diff_key' => 0, 'array_diff_uassoc' => 0, 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 index 1c8530ec3b..1028ae2fa1 100644 --- a/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php @@ -4,27 +4,21 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\Accessory\AccessoryArrayListType; -use PHPStan\Type\Accessory\NonEmptyArrayType; -use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; -use PHPStan\Type\IntegerType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use function count; +#[AutowiredService] final class ArrayChunkFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - private const FINITE_TYPES_LIMIT = 5; - public function __construct(private PhpVersion $phpVersion) { } @@ -41,68 +35,19 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $arrayType = $scope->getType($functionCall->getArgs()[0]->value); - $lengthType = $scope->getType($functionCall->getArgs()[1]->value); - if (isset($functionCall->getArgs()[2])) { - $preserveKeysType = $scope->getType($functionCall->getArgs()[2]->value); - $preserveKeys = $preserveKeysType instanceof ConstantBooleanType ? $preserveKeysType->getValue() : null; - } else { - $preserveKeys = false; + 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(); } - if (!$arrayType->isArray()->yes()) { - return null; - } - - if ($preserveKeys !== null) { - $constantArrays = $arrayType->getConstantArrays(); - $biggerOne = IntegerRangeType::fromInterval(1, null); - $finiteTypes = $lengthType->getFiniteTypes(); - if (count($constantArrays) > 0 - && $biggerOne->isSuperTypeOf($lengthType)->yes() - && count($finiteTypes) < self::FINITE_TYPES_LIMIT - ) { - $results = []; - foreach ($constantArrays as $constantArray) { - foreach ($finiteTypes as $finiteType) { - if (!$finiteType instanceof ConstantIntegerType || $finiteType->getValue() < 1) { - return null; - } - - $results[] = $constantArray->chunk($finiteType->getValue(), $preserveKeys); - } - } - - return TypeCombinator::union(...$results); - } - } - - $chunkType = self::getChunkType($arrayType, $preserveKeys); - - $resultType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $chunkType)); - if ($arrayType->isIterableAtLeastOnce()->yes()) { - $resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType()); - } - - return $resultType; - } - - private static function getChunkType(Type $type, ?bool $preserveKeys): Type - { - if ($preserveKeys === null) { - $chunkType = new ArrayType(TypeCombinator::union($type->getIterableKeyType(), new IntegerType()), $type->getIterableValueType()); - } elseif ($preserveKeys) { - $chunkType = $type; - } else { - $chunkType = new ArrayType(new IntegerType(), $type->getIterableValueType()); - $chunkType = AccessoryArrayListType::intersectWith($chunkType); - } + $preserveKeysType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : new ConstantBooleanType(false); - return TypeCombinator::intersect($chunkType, new NonEmptyArrayType()); + return $arrayType->chunkArray($lengthType, $preserveKeysType->isTrue()); } } diff --git a/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php index a7edf8777c..4fec6cfe3e 100644 --- a/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php @@ -4,27 +4,20 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; -use PHPStan\Php\PhpVersion; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -use PHPStan\ShouldNotHappenException; -use PHPStan\TrinaryLogic; -use PHPStan\Type\Accessory\AccessoryArrayListType; -use PHPStan\Type\Accessory\NonEmptyArrayType; -use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntegerType; -use PHPStan\Type\MixedType; -use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use function count; -class ArrayColumnFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayColumnFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) + public function __construct( + private ArrayColumnHelper $arrayColumnHelper, + ) { } @@ -42,171 +35,17 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($functionCall->getArgs()[0]->value); $columnType = $scope->getType($functionCall->getArgs()[1]->value); - $indexType = $numArgs >= 3 ? $scope->getType($functionCall->getArgs()[2]->value) : null; + $indexType = $numArgs >= 3 ? $scope->getType($functionCall->getArgs()[2]->value) : new NullType(); $constantArrayTypes = $arrayType->getConstantArrays(); if (count($constantArrayTypes) === 1) { - $type = $this->handleConstantArray($constantArrayTypes[0], $columnType, $indexType, $scope); + $type = $this->arrayColumnHelper->handleConstantArray($constantArrayTypes[0], $columnType, $indexType, $scope); if ($type !== null) { return $type; } } - return $this->handleAnyArray($arrayType, $columnType, $indexType, $scope); - } - - private function handleAnyArray(Type $arrayType, Type $columnType, ?Type $indexType, Scope $scope): Type - { - $iterableAtLeastOnce = $arrayType->isIterableAtLeastOnce(); - if ($iterableAtLeastOnce->no()) { - return new ConstantArrayType([], []); - } - - $iterableValueType = $arrayType->getIterableValueType(); - $returnValueType = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, false); - - if ($returnValueType === null) { - $returnValueType = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, true); - $iterableAtLeastOnce = TrinaryLogic::createMaybe(); - if ($returnValueType === null) { - throw new ShouldNotHappenException(); - } - } - - if ($returnValueType instanceof NeverType) { - return new ConstantArrayType([], []); - } - - if ($indexType !== null) { - $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, false); - if ($type !== null) { - $returnKeyType = $type; - } else { - $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, true); - if ($type !== null) { - $returnKeyType = TypeCombinator::union($type, new IntegerType()); - } else { - $returnKeyType = new IntegerType(); - } - } - } else { - $returnKeyType = new IntegerType(); - } - - $returnType = new ArrayType($this->castToArrayKeyType($returnKeyType), $returnValueType); - - if ($iterableAtLeastOnce->yes()) { - $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); - } - if ($indexType === null) { - $returnType = AccessoryArrayListType::intersectWith($returnType); - } - - return $returnType; - } - - private function handleConstantArray(ConstantArrayType $arrayType, Type $columnType, ?Type $indexType, Scope $scope): ?Type - { - $builder = ConstantArrayTypeBuilder::createEmpty(); - - foreach ($arrayType->getValueTypes() as $i => $iterableValueType) { - $valueType = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, false); - if ($valueType === null) { - return null; - } - if ($valueType instanceof NeverType) { - continue; - } - - if ($indexType !== null) { - $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, false); - if ($type !== null) { - $keyType = $type; - } else { - $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, true); - if ($type !== null) { - $keyType = TypeCombinator::union($type, new IntegerType()); - } else { - $keyType = null; - } - } - } else { - $keyType = null; - } - - if ($keyType !== null) { - $keyType = $this->castToArrayKeyType($keyType); - } - $builder->setOffsetValueType($keyType, $valueType, $arrayType->isOptionalKey($i)); - } - - return $builder->getArray(); - } - - private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $scope, bool $allowMaybe): ?Type - { - $offsetIsNull = $offsetOrProperty->isNull(); - if ($offsetIsNull->yes()) { - return $type; - } - - $returnTypes = []; - - if ($offsetIsNull->maybe()) { - $returnTypes[] = $type; - } - - if (!$type->canAccessProperties()->no()) { - $propertyTypes = $offsetOrProperty->getConstantStrings(); - if ($propertyTypes === []) { - return new MixedType(); - } - foreach ($propertyTypes as $propertyType) { - $propertyName = $propertyType->getValue(); - $hasProperty = $type->hasProperty($propertyName); - if ($hasProperty->maybe()) { - return $allowMaybe ? new MixedType() : null; - } - if (!$hasProperty->yes()) { - continue; - } - - $returnTypes[] = $type->getProperty($propertyName, $scope)->getReadableType(); - } - } - - if ($type->isOffsetAccessible()->yes()) { - $hasOffset = $type->hasOffsetValueType($offsetOrProperty); - if (!$allowMaybe && $hasOffset->maybe()) { - return null; - } - if (!$hasOffset->no()) { - $returnTypes[] = $type->getOffsetValueType($offsetOrProperty); - } - } - - if ($returnTypes === []) { - return new NeverType(); - } - - return TypeCombinator::union(...$returnTypes); - } - - private function castToArrayKeyType(Type $type): Type - { - $isArray = $type->isArray(); - if ($isArray->yes()) { - return $this->phpVersion->throwsTypeErrorForInternalFunctions() ? new NeverType() : new IntegerType(); - } - if ($isArray->no()) { - return $type->toArrayKey(); - } - $withoutArrayType = TypeCombinator::remove($type, new ArrayType(new MixedType(), new MixedType())); - $keyType = $withoutArrayType->toArrayKey(); - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { - return $keyType; - } - return TypeCombinator::union($keyType, new IntegerType()); + 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..22e7eb69d0 --- /dev/null +++ b/src/Type/Php/ArrayColumnHelper.php @@ -0,0 +1,198 @@ +isIterableAtLeastOnce(); + if ($iterableAtLeastOnce->no()) { + return [new NeverType(), $iterableAtLeastOnce]; + } + + $iterableValueType = $arrayType->getIterableValueType(); + $returnValueType = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, false); + + if ($returnValueType === null) { + $returnValueType = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, true); + $iterableAtLeastOnce = TrinaryLogic::createMaybe(); + if ($returnValueType === null) { + throw new ShouldNotHappenException(); + } + } + + return [$returnValueType, $iterableAtLeastOnce]; + } + + public function getReturnIndexType(Type $arrayType, Type $indexType, Scope $scope): Type + { + if (!$indexType->isNull()->yes()) { + $iterableValueType = $arrayType->getIterableValueType(); + + $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, false); + if ($type !== null) { + return $type; + } + + $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, true); + if ($type !== null) { + return TypeCombinator::union($type, new IntegerType()); + } + } + + return new IntegerType(); + } + + public function handleAnyArray(Type $arrayType, Type $columnType, Type $indexType, Scope $scope): Type + { + [$returnValueType, $iterableAtLeastOnce] = $this->getReturnValueType($arrayType, $columnType, $scope); + if ($returnValueType instanceof NeverType) { + return new ConstantArrayType([], []); + } + + $returnKeyType = $this->getReturnIndexType($arrayType, $indexType, $scope); + $returnType = new ArrayType($this->castToArrayKeyType($returnKeyType), $returnValueType); + + if ($iterableAtLeastOnce->yes()) { + $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); + } + if ($indexType->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 = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, false); + if ($valueType === null) { + return null; + } + if ($valueType instanceof NeverType) { + continue; + } + + if (!$indexType->isNull()->yes()) { + $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, false); + if ($type !== null) { + $keyType = $type; + } else { + $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, true); + if ($type !== null) { + $keyType = TypeCombinator::union($type, new IntegerType()); + } else { + $keyType = null; + } + } + } else { + $keyType = null; + } + + if ($keyType !== null) { + $keyType = $this->castToArrayKeyType($keyType); + } + $builder->setOffsetValueType($keyType, $valueType, $arrayType->isOptionalKey($i)); + } + + return $builder->getArray(); + } + + private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $scope, bool $allowMaybe): ?Type + { + $offsetIsNull = $offsetOrProperty->isNull(); + if ($offsetIsNull->yes()) { + return $type; + } + + $returnTypes = []; + + if ($offsetIsNull->maybe()) { + $returnTypes[] = $type; + } + + if (!$type->canAccessProperties()->no()) { + $propertyTypes = $offsetOrProperty->getConstantStrings(); + if ($propertyTypes === []) { + return new MixedType(); + } + foreach ($propertyTypes as $propertyType) { + $propertyName = $propertyType->getValue(); + $hasProperty = $type->hasProperty($propertyName); + if ($hasProperty->maybe()) { + return $allowMaybe ? new MixedType() : null; + } + if (!$hasProperty->yes()) { + continue; + } + + $returnTypes[] = $type->getProperty($propertyName, $scope)->getReadableType(); + } + } + + if ($type->isOffsetAccessible()->yes()) { + $hasOffset = $type->hasOffsetValueType($offsetOrProperty); + if (!$allowMaybe && $hasOffset->maybe()) { + return null; + } + if (!$hasOffset->no()) { + $returnTypes[] = $type->getOffsetValueType($offsetOrProperty); + } + } + + if ($returnTypes === []) { + return new NeverType(); + } + + return TypeCombinator::union(...$returnTypes); + } + + private function castToArrayKeyType(Type $type): Type + { + $isArray = $type->isArray(); + if ($isArray->yes()) { + return $this->phpVersion->throwsTypeErrorForInternalFunctions() ? new NeverType() : new IntegerType(); + } + if ($isArray->no()) { + return $type->toArrayKey(); + } + $withoutArrayType = TypeCombinator::remove($type, new ArrayType(new MixedType(), new MixedType())); + $keyType = $withoutArrayType->toArrayKey(); + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return $keyType; + } + return TypeCombinator::union($keyType, new IntegerType()); + } + +} diff --git a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php index 62e1d8435b..663859a069 100644 --- a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php @@ -5,11 +5,13 @@ 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\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; @@ -22,7 +24,8 @@ use PHPStan\Type\UnionType; use function count; -class ArrayCombineFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayCombineFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private PhpVersion $phpVersion) @@ -62,11 +65,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $keyTypes = $this->sanitizeConstantArrayKeyTypes($keyTypes); if ($keyTypes !== null) { - return new ConstantArrayType( - $keyTypes, - $valueTypes, - $keysParamType->getNextAutoIndexes(), - ); + $builder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($keyTypes as $i => $keyType) { + $valueType = $valueTypes[$i]; + $builder->setOffsetValueType($keyType, $valueType); + } + + return $builder->getArray(); } } diff --git a/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php b/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php index 3ca47235f8..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\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ArrayCurrentDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayCurrentDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php index 2e85f59099..63cf6d9cf4 100644 --- a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.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\Accessory\AccessoryArrayListType; @@ -20,7 +21,8 @@ use PHPStan\Type\TypeCombinator; use function count; -class ArrayFillFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayFillFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { private const MAX_SIZE_USE_CONSTANT_ARRAY = 100; @@ -78,7 +80,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $resultType = new ArrayType(new IntegerType(), $valueType); if ((new ConstantIntegerType(0))->isSuperTypeOf($startIndexType)->yes()) { - $resultType = AccessoryArrayListType::intersectWith($resultType); + $resultType = TypeCombinator::intersect($resultType, new AccessoryArrayListType()); } if (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($numberType)->yes()) { $resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType()); diff --git a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php index 785991b2c1..bf05500cba 100644 --- a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.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\DynamicFunctionReturnTypeExtension; @@ -12,7 +13,8 @@ use PHPStan\Type\Type; use function count; -class ArrayFillKeysFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayFillKeysFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private PhpVersion $phpVersion) 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/ArrayFilterFunctionReturnTypeReturnTypeExtension.php b/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php similarity index 54% rename from src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php rename to src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php index d697380439..04ef90fef5 100644 --- a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php @@ -6,23 +6,25 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrowFunction; use PhpParser\Node\Expr\Closure; -use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\Error; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Name; -use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\FunctionReflection; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; -use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; @@ -33,24 +35,28 @@ use PHPStan\Type\TypeUtils; use function array_map; use function count; +use function in_array; use function is_string; -use function strtolower; +use function sprintf; use function substr; -class ArrayFilterFunctionReturnTypeReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayFilterFunctionReturnTypeHelper { - public function isFunctionSupported(FunctionReflection $functionReflection): bool + private const USE_BOTH = 1; + private const USE_KEY = 2; + private const USE_ITEM = 3; + + public function __construct( + private ReflectionProvider $reflectionProvider, + private PhpVersion $phpVersion, + ) { - return $functionReflection->getName() === 'array_filter'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getType(Scope $scope, ?Expr $arrayArg, ?Expr $callbackArg, ?Expr $flagArg): Type { - $arrayArg = $functionCall->getArgs()[0]->value ?? null; - $callbackArg = $functionCall->getArgs()[1]->value ?? null; - $flagArg = $functionCall->getArgs()[2]->value ?? null; - if ($arrayArg === null) { return new ArrayType(new MixedType(), new MixedType()); } @@ -65,83 +71,86 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } 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 || ($callbackArg instanceof ConstFetch && strtolower($callbackArg->name->getParts()[0]) === 'null')) { + if ($callbackArg === null || $scope->getType($callbackArg)->isNull()->yes()) { return TypeCombinator::union( ...array_map([$this, 'removeFalsey'], $arrayArgType->getArrays()), ); } - if ($flagArg === null) { - if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) { - $statement = $callbackArg->stmts[0]; - if ($statement instanceof Return_ && $statement->expr !== null) { - return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, null, $statement->expr); - } - } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { - return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, null, $callbackArg->expr); - } elseif ($callbackArg instanceof String_) { - $funcName = self::createFunctionName($callbackArg->value); - if ($funcName === null) { - return new ErrorType(); - } - - $itemVar = new Variable('item'); - $expr = new FuncCall($funcName, [new Arg($itemVar)]); - return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, null, $expr); - } + $mode = $this->determineMode($flagArg, $scope); + if ($mode === null) { + return new ArrayType($keyType, $itemType); } - if ($flagArg instanceof ConstFetch && $flagArg->name->getParts()[0] === 'ARRAY_FILTER_USE_KEY') { - if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) { - $statement = $callbackArg->stmts[0]; - if ($statement instanceof Return_ && $statement->expr !== null) { - return $this->filterByTruthyValue($scope, null, $arrayArgType, $callbackArg->params[0]->var, $statement->expr); + 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; } - } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { - return $this->filterByTruthyValue($scope, null, $arrayArgType, $callbackArg->params[0]->var, $callbackArg->expr); - } elseif ($callbackArg instanceof String_) { - $funcName = self::createFunctionName($callbackArg->value); - if ($funcName === null) { - return new ErrorType(); - } - - $keyVar = new Variable('key'); - $expr = new FuncCall($funcName, [new Arg($keyVar)]); - return $this->filterByTruthyValue($scope, null, $arrayArgType, $keyVar, $expr); + 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; + } - if ($flagArg instanceof ConstFetch && $flagArg->name->getParts()[0] === 'ARRAY_FILTER_USE_BOTH') { - if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) { - $statement = $callbackArg->stmts[0]; - if ($statement instanceof Return_ && $statement->expr !== null) { - return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, $callbackArg->params[1]->var ?? null, $statement->expr); - } - } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { - return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, $callbackArg->params[1]->var ?? null, $callbackArg->expr); - } elseif ($callbackArg instanceof String_) { - $funcName = self::createFunctionName($callbackArg->value); - if ($funcName === null) { - return new ErrorType(); + $expr = new FuncCall($funcName, $args); + $results[] = $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr); } - - $itemVar = new Variable('item'); - $keyVar = new Variable('key'); - $expr = new FuncCall($funcName, [new Arg($itemVar), new Arg($keyVar)]); - return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr); + return TypeCombinator::union(...$results); } } return new ArrayType($keyType, $itemType); } - public function removeFalsey(Type $type): Type + private function removeFalsey(Type $type): Type { $falseyTypes = StaticTypeFactory::falsey(); @@ -192,9 +201,11 @@ private function filterByTruthyValue(Scope $scope, Error|Variable|null $itemVar, $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; } @@ -232,7 +243,7 @@ private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type throw new ShouldNotHappenException(); } $itemVarName = $itemVar->name; - $scope = $scope->assignVariable($itemVarName, $itemType, new MixedType()); + $scope = $scope->assignVariable($itemVarName, $itemType, new MixedType(), TrinaryLogic::createYes()); } $keyVarName = null; @@ -241,7 +252,7 @@ private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type throw new ShouldNotHappenException(); } $keyVarName = $keyVar->name; - $scope = $scope->assignVariable($keyVarName, $keyType, new MixedType()); + $scope = $scope->assignVariable($keyVarName, $keyType, new MixedType(), TrinaryLogic::createYes()); } $booleanResult = $scope->getType($expr)->toBoolean(); @@ -254,7 +265,7 @@ private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type return [ $keyVarName !== null ? $scope->getVariableType($keyVarName) : $keyType, $itemVarName !== null ? $scope->getVariableType($itemVarName) : $itemType, - !$booleanResult instanceof ConstantBooleanType, + !$booleanResult->isTrue()->yes(), ]; } @@ -277,4 +288,63 @@ private static function createFunctionName(string $funcName): ?Name 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/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 1beb76de40..3e82909d22 100644 --- a/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php @@ -4,15 +4,19 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function count; -class ArrayFlipFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayFlipFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private PhpVersion $phpVersion) @@ -35,7 +39,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - return $arrayType->flipArray(); + $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 index 10335fa3e8..c6212c02cc 100644 --- a/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.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\DynamicFunctionReturnTypeExtension; @@ -14,7 +15,8 @@ use function array_slice; use function count; -class ArrayIntersectKeyFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayIntersectKeyFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private PhpVersion $phpVersion) diff --git a/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php index 16e343e12d..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\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ArrayKeyDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayKeyDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index d2d5ed8360..c9de826666 100644 --- a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php @@ -12,8 +12,10 @@ 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; @@ -23,7 +25,8 @@ use function count; use function in_array; -class ArrayKeyExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class ArrayKeyExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -55,13 +58,23 @@ public function specifyTypes( } $key = $node->getArgs()[0]->value; $array = $node->getArgs()[1]->value; - $keyType = $scope->getType($key); + $keyType = $scope->getType($key)->toArrayKey(); $arrayType = $scope->getType($array); - if (!$keyType instanceof ConstantIntegerType + if ( + !$keyType instanceof ConstantIntegerType && !$keyType instanceof ConstantStringType - && !$arrayType->isIterableAtLeastOnce()->no()) { + ) { 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(); @@ -73,7 +86,6 @@ public function specifyTypes( $key, $arrayKeyType, $context, - false, $scope, ); @@ -86,10 +98,8 @@ public function specifyTypes( $arrayDimFetch, $arrayType->getIterableValueType(), $context, - false, $scope, - new Identical($arrayDimFetch, new ConstFetch(new Name('__PHPSTAN_FAUX_CONSTANT'))), - )); + ))->setRootExpr(new Identical($arrayDimFetch, new ConstFetch(new Name('__PHPSTAN_FAUX_CONSTANT')))); } return new SpecifiedTypes(); @@ -108,7 +118,6 @@ public function specifyTypes( $array, $type, $context, - false, $scope, ); } diff --git a/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php index 06fcfb7c74..64fea966d6 100644 --- a/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php @@ -4,13 +4,15 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ArrayKeyFirstDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayKeyFirstDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php index c1684afa9d..c3865ada3c 100644 --- a/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php @@ -4,13 +4,15 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ArrayKeyLastDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayKeyLastDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php index 2139222a9a..bb7a7bea88 100644 --- a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php @@ -4,8 +4,10 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; +use PHPStan\TrinaryLogic; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; @@ -13,7 +15,8 @@ use function count; use function strtolower; -class ArrayKeysFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayKeysFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private PhpVersion $phpVersion) @@ -27,15 +30,27 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - if (count($functionCall->getArgs()) !== 1) { + $args = $functionCall->getArgs(); + if (count($args) < 1) { return null; } - $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + $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 $arrayType->getKeysArray(); } diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index af0f49d231..a2831eeb4b 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -2,8 +2,11 @@ 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\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -13,14 +16,16 @@ 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 DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayMapFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -30,26 +35,78 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - if (count($functionCall->getArgs()) < 2) { + $numArgs = count($functionCall->getArgs()); + if ($numArgs < 2) { return null; } $singleArrayArgument = !isset($functionCall->getArgs()[2]); - $callableType = $scope->getType($functionCall->getArgs()[0]->value); + $callback = $functionCall->getArgs()[0]->value; + $callableType = $scope->getType($callback); $callableIsNull = $callableType->isNull()->yes(); if ($callableType->isCallable()->yes()) { - $valueTypes = [new NeverType()]; - foreach ($callableType->getCallableParametersAcceptors($scope) as $parametersAcceptor) { - $valueTypes[] = $parametersAcceptor->getReturnType(); - } - $valueType = TypeCombinator::union(...$valueTypes); + $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), - $scope->getType($arg->value)->getIterableValueType(), + $offsetValueType, ); } $valueType = $arrayBuilder->getArray(); @@ -70,16 +127,19 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, 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, - $valueType, + $scope->getType(new FuncCall($callback, [ + new Node\Arg(new TypeExpr($valueTypes[$i])), + ])), $constantArray->isOptionalKey($i), ); } $returnedArray = $returnedArrayBuilder->getArray(); if ($constantArray->isList()->yes()) { - $returnedArray = AccessoryArrayListType::intersectWith($returnedArray); + $returnedArray = TypeCombinator::intersect($returnedArray, new AccessoryArrayListType()); } $arrayTypes[] = $returnedArray; } @@ -106,7 +166,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $mappedArrayType = TypeCombinator::intersect(new ArrayType( new IntegerType(), $valueType, - ), ...TypeUtils::getAccessoryTypes($arrayType)); + ), new AccessoryArrayListType(), ...TypeUtils::getAccessoryTypes($arrayType)); } if ($arrayType->isIterableAtLeastOnce()->yes()) { diff --git a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php index 5bde2a454e..1d4f7a1122 100644 --- a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php @@ -4,14 +4,16 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\NeverType; @@ -21,7 +23,8 @@ use function count; use function in_array; -class ArrayMergeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayMergeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -39,58 +42,53 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $argTypes = []; $optionalArgTypes = []; - $allConstant = true; foreach ($args as $arg) { $argType = $scope->getType($arg->value); if ($arg->unpack) { - if ($argType instanceof ConstantArrayType) { - $argTypesFound = $argType->getValueTypes(); - } else { - $argTypesFound = [$argType->getIterableValueType()]; - } - - foreach ($argTypesFound as $argTypeFound) { - $argTypes[] = $argTypeFound; - if ($argTypeFound instanceof ConstantArrayType) { - continue; + if ($argType->isConstantArray()->yes()) { + foreach ($argType->getConstantArrays() as $constantArray) { + foreach ($constantArray->getValueTypes() as $valueType) { + $argTypes[] = $valueType; + } } - $allConstant = false; + } else { + $argTypes[] = $argType->getIterableValueType(); } if (!$argType->isIterableAtLeastOnce()->yes()) { // unpacked params can be empty, making them optional $optionalArgTypesOffset = count($argTypes) - 1; - foreach (array_keys($argTypesFound) as $key) { + foreach (array_keys($argTypes) as $key) { $optionalArgTypes[] = $optionalArgTypesOffset + $key; } } } else { $argTypes[] = $argType; - if (!$argType instanceof ConstantArrayType) { - $allConstant = false; - } } } - if ($allConstant) { + $allConstant = TrinaryLogic::createYes()->lazyAnd( + $argTypes, + static fn (Type $argType) => $argType->isConstantArray(), + ); + + if ($allConstant->yes()) { $newArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach ($argTypes as $argType) { - if (!$argType instanceof ConstantArrayType) { - throw new ShouldNotHappenException(); + /** @var array $keyTypes */ + $keyTypes = []; + foreach ($argType->getConstantArrays() as $constantArray) { + foreach ($constantArray->getKeyTypes() as $keyType) { + $keyTypes[$keyType->getValue()] = $keyType; + } } - $keyTypes = $argType->getKeyTypes(); - $valueTypes = $argType->getValueTypes(); - $optionalKeys = $argType->getOptionalKeys(); - - foreach ($keyTypes as $k => $keyType) { - $isOptional = in_array($k, $optionalKeys, true); - + foreach ($keyTypes as $keyType) { $newArrayBuilder->setOffsetValueType( $keyType instanceof ConstantIntegerType ? null : $keyType, - $valueTypes[$k], - $isOptional, + $argType->getOffsetValueType($keyType), + !$argType->hasOffsetValueType($keyType)->yes(), ); } } @@ -132,7 +130,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); } if ($isList) { - $arrayType = AccessoryArrayListType::intersectWith($arrayType); + $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType()); } return $arrayType; diff --git a/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php b/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php index dff732d028..a0f49b4ca3 100644 --- a/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -11,7 +12,8 @@ use PHPStan\Type\TypeCombinator; use function in_array; -class ArrayNextDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayNextDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool 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 cd12f678d1..eef5642028 100644 --- a/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -12,7 +13,8 @@ use function count; use function in_array; -class ArrayPointerFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayPointerFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { /** @var string[] */ diff --git a/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php b/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php index 64935edaea..61bfdf69e6 100644 --- a/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php @@ -4,13 +4,15 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ArrayPopFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayPopFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php b/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php index 315acd8488..f61d48033e 100644 --- a/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -16,7 +17,8 @@ use PHPStan\Type\UnionType; use function count; -class ArrayRandFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayRandFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php index 80bd5866ec..7f955e6baa 100644 --- a/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\TrinaryLogic; @@ -13,7 +14,8 @@ use PHPStan\Type\TypeCombinator; use function count; -class ArrayReduceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayReduceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/ArrayReplaceFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReplaceFunctionReturnTypeExtension.php index 94584e3583..fdb8066f60 100644 --- a/src/Type/Php/ArrayReplaceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReplaceFunctionReturnTypeExtension.php @@ -4,16 +4,27 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; +use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_keys; use function count; +use function in_array; use function strtolower; -class ArrayReplaceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayReplaceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,54 +34,107 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $arrayTypes = $this->collectArrayTypes($functionCall, $scope); + $args = $functionCall->getArgs(); - if (count($arrayTypes) === 0) { + if (!isset($args[0])) { return null; } - return $this->getResultType(...$arrayTypes); - } + $argTypes = []; + $optionalArgTypes = []; + foreach ($args as $arg) { + $argType = $scope->getType($arg->value); - private function getResultType(Type ...$arrayTypes): Type - { - $keyTypes = []; - $valueTypes = []; - $nonEmptyArray = false; - foreach ($arrayTypes as $arrayType) { - if (!$nonEmptyArray && $arrayType->isIterableAtLeastOnce()->yes()) { - $nonEmptyArray = true; + 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; } - - $keyTypes[] = $arrayType->getIterableKeyType(); - $valueTypes[] = $arrayType->getIterableValueType(); } - $keyType = TypeCombinator::union(...$keyTypes); - $valueType = TypeCombinator::union(...$valueTypes); + $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(), + ); + } + } - $arrayType = new ArrayType($keyType, $valueType); - return $nonEmptyArray ? TypeCombinator::intersect($arrayType, new NonEmptyArrayType()) : $arrayType; - } + return $newArrayBuilder->getArray(); + } - /** - * @return Type[] - */ - private function collectArrayTypes(FuncCall $functionCall, Scope $scope): array - { - $args = $functionCall->getArgs(); + $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; + } - $arrayTypes = []; - foreach ($args as $arg) { - $argType = $scope->getType($arg->value); - if (!$argType->isArray()->yes()) { + if (in_array($key, $optionalArgTypes, true) || !$argType->isIterableAtLeastOnce()->yes()) { continue; } - $arrayTypes[] = $arg->unpack ? $argType->getIterableValueType() : $argType; + $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 $arrayTypes; + return $arrayType; } } diff --git a/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php index ef16c66fbf..0c90c7bfaa 100644 --- a/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php @@ -4,16 +4,23 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use function count; -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'; @@ -26,24 +33,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $type = $scope->getType($functionCall->getArgs()[0]->value); - $preserveKeysType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : new NeverType(); - $preserveKeys = $preserveKeysType->isTrue()->yes(); - - if (!$type->isArray()->yes()) { - return null; + if ($type->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - $constantArrays = $type->getConstantArrays(); - if (count($constantArrays) > 0) { - $results = []; - foreach ($constantArrays as $constantArray) { - $results[] = $constantArray->reverse($preserveKeys); - } - - return TypeCombinator::union(...$results); - } + $preserveKeysType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : new ConstantBooleanType(false); - return $type; + return $type->reverseArray($preserveKeysType->isTrue()); } } diff --git a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php index 8560faf5a9..b392e91340 100644 --- a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.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\Constant\ConstantBooleanType; @@ -11,9 +12,9 @@ use PHPStan\Type\NeverType; use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use function count; +#[AutowiredService] final class ArraySearchFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -39,20 +40,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($argsCount < 3) { - return TypeCombinator::union($haystackArgType->getIterableKeyType(), new ConstantBooleanType(false)); - } - - $strictArgType = $scope->getType($functionCall->getArgs()[2]->value); - if (!$strictArgType instanceof ConstantBooleanType || $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); - } - return $haystackArgType->searchArray($needleArgType); + return $haystackArgType->searchArray($needleArgType, $strictArgType->isTrue()); } } diff --git a/src/Type/Php/ArraySearchFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArraySearchFunctionTypeSpecifyingExtension.php index 8571c82ed3..38d8909e9b 100644 --- a/src/Type/Php/ArraySearchFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArraySearchFunctionTypeSpecifyingExtension.php @@ -8,15 +8,14 @@ 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 PHPStan\Type\TypeCombinator; use function strtolower; -class ArraySearchFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class ArraySearchFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -45,9 +44,8 @@ public function specifyTypes( return $this->typeSpecifier->create( $arrayArg, - TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), new NonEmptyArrayType()), + new NonEmptyArrayType(), $context, - false, $scope, ); } diff --git a/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php b/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php index 47552e875e..b961e624e0 100644 --- a/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php @@ -4,13 +4,15 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ArrayShiftFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayShiftFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php index 811727ae39..5ac2ba4606 100644 --- a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php @@ -4,19 +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\Type\Accessory\NonEmptyArrayType; -use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use function count; -class ArraySliceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArraySliceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_slice'; @@ -25,49 +30,20 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $args = $functionCall->getArgs(); - if (count($args) < 1) { + if (count($args) < 2) { return null; } - $valueType = $scope->getType($args[0]->value); - if (!$valueType->isArray()->yes()) { - return null; + $arrayType = $scope->getType($args[0]->value); + if ($arrayType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - $offsetType = isset($args[1]) ? $scope->getType($args[1]->value) : null; - $limitType = isset($args[2]) ? $scope->getType($args[2]->value) : null; - - $constantArrays = $valueType->getConstantArrays(); - if (count($constantArrays) > 0) { - $preserveKeysType = isset($args[3]) ? $scope->getType($args[3]->value) : null; - - $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : 0; - $limit = $limitType instanceof ConstantIntegerType ? $limitType->getValue() : null; - $preserveKeys = $preserveKeysType !== null && $preserveKeysType->isTrue()->yes(); - - $results = []; - foreach ($constantArrays as $constantArray) { - $results[] = $constantArray->slice($offset, $limit, $preserveKeys); - } - - return TypeCombinator::union(...$results); - } - - if ($valueType->isIterableAtLeastOnce()->yes()) { - $optionalOffsetsType = TypeCombinator::union($valueType, new ConstantArrayType([], [])); - - $zero = new ConstantIntegerType(0); - if ( - ($offsetType === null || $zero->isSuperTypeOf($offsetType)->yes()) - && ($limitType === null || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($limitType)->yes()) - ) { - return TypeCombinator::intersect($optionalOffsetsType, new NonEmptyArrayType()); - } - - return $optionalOffsetsType; - } + $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 $valueType; + return $arrayType->sliceArray($offsetType, $lengthType, $preserveKeysType->isTrue()); } } diff --git a/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php index 45cff582a2..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\Type\ArrayType; +use PHPStan\TrinaryLogic; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; +use function count; -class ArraySpliceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArraySpliceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_splice'; @@ -23,13 +33,20 @@ public function getTypeFromFunctionCall( Scope $scope, ): ?Type { - if (!isset($functionCall->getArgs()[0])) { + $args = $functionCall->getArgs(); + if (count($args) < 2) { return null; } - $arrayArg = $scope->getType($functionCall->getArgs()[0]->value); + $arrayType = $scope->getType($args[0]->value); + if ($arrayType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + } + + $offsetType = $scope->getType($args[1]->value); + $lengthType = isset($args[2]) ? $scope->getType($args[2]->value) : new NullType(); - return 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 b60730e828..061f3d4dff 100644 --- a/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php @@ -5,8 +5,9 @@ use PhpParser\Node\Expr\BinaryOp\Mul; use PhpParser\Node\Expr\BinaryOp\Plus; use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Scalar\LNumber; +use PhpParser\Node\Scalar\Int_; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantIntegerType; @@ -16,6 +17,7 @@ use PHPStan\Type\TypeCombinator; use function count; +#[AutowiredService] final class ArraySumFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -35,7 +37,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if (count($argType->getConstantArrays()) > 0) { foreach ($argType->getConstantArrays() as $constantArray) { - $node = new LNumber(0); + $node = new Int_(0); foreach ($constantArray->getValueTypes() as $i => $type) { if ($constantArray->isOptionalKey($i)) { diff --git a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php index 4573e7d89d..794b8df7ba 100644 --- a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.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\DynamicFunctionReturnTypeExtension; @@ -13,7 +14,8 @@ use function count; use function strtolower; -class ArrayValuesFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ArrayValuesFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private PhpVersion $phpVersion) diff --git a/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php b/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php index 413a163535..adc436e584 100644 --- a/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php @@ -8,10 +8,12 @@ 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 TypeSpecifier $typeSpecifier; diff --git a/src/Type/Php/AssertThrowTypeExtension.php b/src/Type/Php/AssertThrowTypeExtension.php index 22bbbc2047..b30bab71cc 100644 --- a/src/Type/Php/AssertThrowTypeExtension.php +++ b/src/Type/Php/AssertThrowTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionThrowTypeExtension; use PHPStan\Type\ObjectType; @@ -11,7 +12,8 @@ use Throwable; use function count; -class AssertThrowTypeExtension implements DynamicFunctionThrowTypeExtension +#[AutowiredService] +final class AssertThrowTypeExtension implements DynamicFunctionThrowTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php b/src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php index 8f4103018d..5ee4ab5cf0 100644 --- a/src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php +++ b/src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php @@ -5,6 +5,7 @@ use BackedEnum; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\Enum\EnumCaseObjectType; @@ -14,7 +15,8 @@ use function count; use function in_array; -class BackedEnumFromMethodDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension +#[AutowiredService] +final class BackedEnumFromMethodDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { public function getClass(): string diff --git a/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php b/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php index bb1ef07430..bf91435e62 100644 --- a/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ 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; @@ -13,7 +14,8 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; -class Base64DecodeDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class Base64DecodeDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/BcMathNumberOperatorTypeSpecifyingExtension.php b/src/Type/Php/BcMathNumberOperatorTypeSpecifyingExtension.php new file mode 100644 index 0000000000..c75ebd2056 --- /dev/null +++ b/src/Type/Php/BcMathNumberOperatorTypeSpecifyingExtension.php @@ -0,0 +1,55 @@ +phpVersion->supportsBcMathNumberOperatorOverloading() || $leftSide instanceof NeverType || $rightSide instanceof NeverType) { + return false; + } + + $bcMathNumberType = new ObjectType('BcMath\Number'); + + return in_array($operatorSigil, ['-', '+', '*', '/', '**', '%'], true) + && ( + $bcMathNumberType->isSuperTypeOf($leftSide)->yes() + || $bcMathNumberType->isSuperTypeOf($rightSide)->yes() + ); + } + + public function specifyType(string $operatorSigil, Type $leftSide, Type $rightSide): Type + { + $bcMathNumberType = new ObjectType('BcMath\Number'); + $otherSide = $bcMathNumberType->isSuperTypeOf($leftSide)->yes() + ? $rightSide + : $leftSide; + + if ( + $otherSide->isInteger()->yes() + || $otherSide->isNumericString()->yes() + || $bcMathNumberType->isSuperTypeOf($otherSide)->yes() + ) { + return $bcMathNumberType; + } + + return new ErrorType(); + } + +} diff --git a/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php b/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php index c10cbe2e58..e34e4d3859 100644 --- a/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php +++ b/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php @@ -5,6 +5,7 @@ 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; @@ -21,7 +22,8 @@ use function in_array; use function is_numeric; -class BcMathStringOrNullReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class BcMathStringOrNullReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private PhpVersion $phpVersion) diff --git a/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php index 9367e20b1b..53cec9a0e6 100644 --- a/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php @@ -11,6 +11,7 @@ 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; @@ -21,7 +22,8 @@ use function in_array; use function ltrim; -class ClassExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class ClassExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -50,7 +52,6 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n ]), new ConstantBooleanType(true), $context, - false, $scope, ); } @@ -64,7 +65,6 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $node->getArgs()[0]->value, $narrowedType, $context, - false, $scope, ); } diff --git a/src/Type/Php/ClassImplementsFunctionReturnTypeExtension.php b/src/Type/Php/ClassImplementsFunctionReturnTypeExtension.php index 754ac737c8..3d1010aff0 100644 --- a/src/Type/Php/ClassImplementsFunctionReturnTypeExtension.php +++ b/src/Type/Php/ClassImplementsFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\TrinaryLogic; @@ -18,7 +19,8 @@ use function count; use function in_array; -class ClassImplementsFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ClassImplementsFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -57,7 +59,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $variant->getReturnType(); } - if ($firstArgType->isClassStringType()->no()) { + if ($firstArgType->isClassString()->no()) { return new ConstantBooleanType(false); } diff --git a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php index 1d0e07a500..580595acd7 100644 --- a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php @@ -5,12 +5,14 @@ use Closure; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\ClosureType; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\Type; -class ClosureBindDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension +#[AutowiredService] +final class ClosureBindDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { public function getClass(): string diff --git a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php index 73c34fa9ed..3e275677e2 100644 --- a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php @@ -5,12 +5,14 @@ use Closure; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\ClosureType; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Type; -class ClosureBindToDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension +#[AutowiredService] +final class ClosureBindToDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string diff --git a/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php b/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php index ed2bd20acd..6674b65374 100644 --- a/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php @@ -5,8 +5,9 @@ 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\ParametersAcceptorWithPhpDocs; use PHPStan\Type\ClosureType; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\ErrorType; @@ -14,7 +15,8 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ClosureFromCallableDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension +#[AutowiredService] +final class ClosureFromCallableDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { public function getClass(): string @@ -47,7 +49,12 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, $variant->isVariadic(), $variant->getTemplateTypeMap(), $variant->getResolvedTemplateTypeMap(), - $variant instanceof ParametersAcceptorWithPhpDocs ? $variant->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $variant instanceof ExtendedParametersAcceptor ? $variant->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + throwPoints: $variant->getThrowPoints(), + impurePoints: $variant->getImpurePoints(), + invalidateExpressions: $variant->getInvalidateExpressions(), + usedVariables: $variant->getUsedVariables(), + acceptsNamedArguments: $variant->acceptsNamedArguments(), ); } diff --git a/src/Type/Php/CompactFunctionReturnTypeExtension.php b/src/Type/Php/CompactFunctionReturnTypeExtension.php index 4c634ad0d1..0d7f40c9f7 100644 --- a/src/Type/Php/CompactFunctionReturnTypeExtension.php +++ b/src/Type/Php/CompactFunctionReturnTypeExtension.php @@ -4,6 +4,8 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; @@ -13,10 +15,14 @@ use function array_merge; use function count; -class CompactFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class CompactFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private bool $checkMaybeUndefinedVariables) + public function __construct( + #[AutowiredParameter] + private bool $checkMaybeUndefinedVariables, + ) { } diff --git a/src/Type/Php/ConstantFunctionReturnTypeExtension.php b/src/Type/Php/ConstantFunctionReturnTypeExtension.php index 1e1075c812..f332c99fc9 100644 --- a/src/Type/Php/ConstantFunctionReturnTypeExtension.php +++ b/src/Type/Php/ConstantFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\ErrorType; @@ -11,7 +12,8 @@ use PHPStan\Type\TypeCombinator; use function count; -class ConstantFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ConstantFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private ConstantHelper $constantHelper) diff --git a/src/Type/Php/ConstantHelper.php b/src/Type/Php/ConstantHelper.php index 7f056f90ec..16e55861bc 100644 --- a/src/Type/Php/ConstantHelper.php +++ b/src/Type/Php/ConstantHelper.php @@ -8,11 +8,13 @@ use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified; +use PHPStan\DependencyInjection\AutowiredService; use function count; use function explode; use function ltrim; -class ConstantHelper +#[AutowiredService] +final class ConstantHelper { public function createExprFromConstantName(string $constantName): ?Expr @@ -24,7 +26,7 @@ public function createExprFromConstantName(string $constantName): ?Expr $classConstParts = explode('::', $constantName); if (count($classConstParts) >= 2) { $fqcn = ltrim($classConstParts[0], '\\'); - if ($fqcn === '') { + if ($fqcn === '' || $classConstParts[1] === '') { return null; } 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 d4c995f933..b87d34c466 100644 --- a/src/Type/Php/CountFunctionReturnTypeExtension.php +++ b/src/Type/Php/CountFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ 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\DynamicFunctionReturnTypeExtension; @@ -12,7 +13,8 @@ use function in_array; use const COUNT_RECURSIVE; -class CountFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class CountFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/CountFunctionTypeSpecifyingExtension.php b/src/Type/Php/CountFunctionTypeSpecifyingExtension.php index 5940c2d803..741c7cc880 100644 --- a/src/Type/Php/CountFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/CountFunctionTypeSpecifyingExtension.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\Accessory\NonEmptyArrayType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use function count; use function in_array; -class CountFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class CountFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -41,7 +43,7 @@ public function specifyTypes( 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 index 112ed30292..04daf2ed55 100644 --- a/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php @@ -9,6 +9,7 @@ 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\Accessory\AccessoryNumericStringType; @@ -21,7 +22,8 @@ use PHPStan\Type\UnionType; use function strtolower; -class CtypeDigitFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class CtypeDigitFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -59,7 +61,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n } $unionType = TypeCombinator::union(...$types); - $specifiedTypes = $this->typeSpecifier->create($exprArg, $unionType, $context, false, $scope); + $specifiedTypes = $this->typeSpecifier->create($exprArg, $unionType, $context, $scope); if ($exprArg instanceof Cast\String_) { $castedType = new UnionType([ @@ -71,7 +73,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n new ConstantBooleanType(true), ]); $specifiedTypes = $specifiedTypes->unionWith( - $this->typeSpecifier->create($exprArg->expr, $castedType, $context, false, $scope), + $this->typeSpecifier->create($exprArg->expr, $castedType, $context, $scope), ); } diff --git a/src/Type/Php/CurlGetinfoFunctionDynamicReturnTypeExtension.php b/src/Type/Php/CurlGetinfoFunctionDynamicReturnTypeExtension.php index b5de9de5f1..89970fad0b 100644 --- a/src/Type/Php/CurlGetinfoFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/CurlGetinfoFunctionDynamicReturnTypeExtension.php @@ -5,6 +5,7 @@ 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\ArrayType; @@ -22,6 +23,7 @@ use PHPStan\Type\TypeUtils; use function count; +#[AutowiredService] final class CurlGetinfoFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { diff --git a/src/Type/Php/CurlInitReturnTypeExtension.php b/src/Type/Php/CurlInitReturnTypeExtension.php deleted file mode 100644 index 813b220159..0000000000 --- a/src/Type/Php/CurlInitReturnTypeExtension.php +++ /dev/null @@ -1,102 +0,0 @@ -getName() === 'curl_init'; - } - - public function getTypeFromFunctionCall( - FunctionReflection $functionReflection, - Node\Expr\FuncCall $functionCall, - Scope $scope, - ): Type - { - $args = $functionCall->getArgs(); - $argsCount = count($args); - $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - $notFalseReturnType = TypeCombinator::remove($returnType, new ConstantBooleanType(false)); - if ($argsCount === 0) { - return $notFalseReturnType; - } - - $urlArgType = $scope->getType($args[0]->value); - if ($urlArgType->isConstantScalarValue()->yes() && (new UnionType([new NullType(), new StringType()]))->isSuperTypeOf($urlArgType)->yes()) { - $urlArgReturnTypes = array_map( - fn ($value) => $this->getUrlArgValueReturnType($value, $returnType, $notFalseReturnType), - $urlArgType->getConstantScalarValues(), - ); - return TypeCombinator::union(...$urlArgReturnTypes); - } - - return $returnType; - } - - private function getUrlArgValueReturnType(mixed $urlArgValue, Type $returnType, Type $notFalseReturnType): Type - { - if ($urlArgValue === null) { - return $notFalseReturnType; - } - if (!is_string($urlArgValue)) { - throw new ShouldNotHappenException(); - } - if (str_contains($urlArgValue, "\0")) { - if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { - // https://github.com/php/php-src/blob/php-7.4.33/ext/curl/interface.c#L112-L115 - return new ConstantBooleanType(false); - } - // https://github.com/php/php-src/blob/php-8.0.0/ext/curl/interface.c#L104-L107 - return new NeverType(); - } - if ($this->phpVersion->isCurloptUrlCheckingFileSchemeWithOpenBasedir()) { - // Before PHP 8.0 an unparsable URL or a file:// scheme would fail if open_basedir is used - // Since we can't detect open_basedir properly, we'll always consider a failure possible if these - // conditions are given - // https://github.com/php/php-src/blob/php-7.4.33/ext/curl/interface.c#L139-L158 - $parsedUrlArgValue = parse_url($urlArgValue); - if ($parsedUrlArgValue === false || (isset($parsedUrlArgValue['scheme']) && strcasecmp($parsedUrlArgValue['scheme'], 'file') === 0)) { - return $returnType; - } - } - if (strlen($urlArgValue) > self::CURL_MAX_INPUT_LENGTH) { - // Since libcurl 7.65.0 this would always fail, but no current PHP version requires it at the moment - // https://github.com/curl/curl/commit/5fc28510a4664f46459d9a40187d81cc08571e60 - return $returnType; - } - return $notFalseReturnType; - } - -} diff --git a/src/Type/Php/DateFormatFunctionReturnTypeExtension.php b/src/Type/Php/DateFormatFunctionReturnTypeExtension.php index bae299abaa..666a376cde 100644 --- a/src/Type/Php/DateFormatFunctionReturnTypeExtension.php +++ b/src/Type/Php/DateFormatFunctionReturnTypeExtension.php @@ -4,13 +4,15 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\StringType; use PHPStan\Type\Type; use function count; -class DateFormatFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class DateFormatFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private DateFunctionReturnTypeHelper $dateFunctionReturnTypeHelper) @@ -26,7 +28,7 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): ?Type + ): Type { if (count($functionCall->getArgs()) < 2) { return new StringType(); diff --git a/src/Type/Php/DateFormatMethodReturnTypeExtension.php b/src/Type/Php/DateFormatMethodReturnTypeExtension.php index f028fbea7b..55d9e28d74 100644 --- a/src/Type/Php/DateFormatMethodReturnTypeExtension.php +++ b/src/Type/Php/DateFormatMethodReturnTypeExtension.php @@ -5,13 +5,15 @@ use DateTimeInterface; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\StringType; use PHPStan\Type\Type; use function count; -class DateFormatMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension +#[AutowiredService] +final class DateFormatMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function __construct(private DateFunctionReturnTypeHelper $dateFunctionReturnTypeHelper) @@ -28,7 +30,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool return $methodReflection->getName() === 'format'; } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { if (count($methodCall->getArgs()) === 0) { return new StringType(); diff --git a/src/Type/Php/DateFunctionReturnTypeExtension.php b/src/Type/Php/DateFunctionReturnTypeExtension.php index 41432d1c3a..c74867e186 100644 --- a/src/Type/Php/DateFunctionReturnTypeExtension.php +++ b/src/Type/Php/DateFunctionReturnTypeExtension.php @@ -4,12 +4,14 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use function count; -class DateFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class DateFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private DateFunctionReturnTypeHelper $dateFunctionReturnTypeHelper) diff --git a/src/Type/Php/DateFunctionReturnTypeHelper.php b/src/Type/Php/DateFunctionReturnTypeHelper.php index d6cadad384..bac485292b 100644 --- a/src/Type/Php/DateFunctionReturnTypeHelper.php +++ b/src/Type/Php/DateFunctionReturnTypeHelper.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Php; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; @@ -17,10 +18,11 @@ use function str_pad; use const STR_PAD_LEFT; -class DateFunctionReturnTypeHelper +#[AutowiredService] +final class DateFunctionReturnTypeHelper { - public function getTypeFromFormatType(Type $formatType, bool $useMicrosec): ?Type + public function getTypeFromFormatType(Type $formatType, bool $useMicrosec): Type { $types = []; foreach ($formatType->getConstantStrings() as $formatString) { @@ -76,8 +78,12 @@ public function buildReturnTypeFromFormat(string $formatString, bool $useMicrose return $this->buildNumericRangeType(0, 1, false); case 'u': return $useMicrosec - ? new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]) + ? 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); diff --git a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php index cd7f63662c..971f593bfc 100644 --- a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php @@ -5,6 +5,7 @@ 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; @@ -14,7 +15,8 @@ use PHPStan\Type\TypeCombinator; use function count; -class DateIntervalConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +#[AutowiredService] +final class DateIntervalConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { public function __construct(private PhpVersion $phpVersion) diff --git a/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php index 1fdcbf4937..c5f597fcdc 100644 --- a/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php @@ -5,6 +5,7 @@ use DateInterval; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; @@ -14,7 +15,8 @@ use function count; use function in_array; -class DateIntervalDynamicReturnTypeExtension +#[AutowiredService] +final class DateIntervalDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { diff --git a/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php b/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php index a492e79388..bcdf1032c7 100644 --- a/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php +++ b/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php @@ -8,6 +8,7 @@ use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\Generic\GenericObjectType; @@ -17,7 +18,8 @@ use PHPStan\Type\Type; use function strtolower; -class DatePeriodConstructorReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension +#[AutowiredService] +final class DatePeriodConstructorReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { public function getClass(): string diff --git a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php index 7b691b6257..2facd03945 100644 --- a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php @@ -6,6 +6,7 @@ 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; @@ -16,7 +17,8 @@ use function count; use function in_array; -class DateTimeConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +#[AutowiredService] +final class DateTimeConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { public function __construct(private PhpVersion $phpVersion) diff --git a/src/Type/Php/DateTimeCreateDynamicReturnTypeExtension.php b/src/Type/Php/DateTimeCreateDynamicReturnTypeExtension.php index bd00c0f12a..c2782272cf 100644 --- a/src/Type/Php/DateTimeCreateDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateTimeCreateDynamicReturnTypeExtension.php @@ -6,6 +6,7 @@ use DateTimeImmutable; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -16,7 +17,8 @@ use function date_create; use function in_array; -class DateTimeCreateDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class DateTimeCreateDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/DateTimeDynamicReturnTypeExtension.php b/src/Type/Php/DateTimeDynamicReturnTypeExtension.php index b18c8534df..eedc6a2fee 100644 --- a/src/Type/Php/DateTimeDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateTimeDynamicReturnTypeExtension.php @@ -6,6 +6,7 @@ use DateTimeImmutable; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -15,7 +16,8 @@ use function count; use function in_array; -class DateTimeDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class DateTimeDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php index d22637d4e5..02c0099c4e 100644 --- a/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php +++ b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php @@ -6,6 +6,7 @@ use DateTimeImmutable; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicMethodThrowTypeExtension; @@ -16,6 +17,7 @@ use function count; use function in_array; +#[AutowiredService] final class DateTimeModifyMethodThrowTypeExtension implements DynamicMethodThrowTypeExtension { diff --git a/src/Type/Php/DateTimeModifyReturnTypeExtension.php b/src/Type/Php/DateTimeModifyReturnTypeExtension.php index 39a327157a..0ed2933856 100644 --- a/src/Type/Php/DateTimeModifyReturnTypeExtension.php +++ b/src/Type/Php/DateTimeModifyReturnTypeExtension.php @@ -16,13 +16,13 @@ use Throwable; use function count; -class DateTimeModifyReturnTypeExtension implements DynamicMethodReturnTypeExtension +final class DateTimeModifyReturnTypeExtension implements DynamicMethodReturnTypeExtension { /** @param class-string $dateTimeClass */ public function __construct( private PhpVersion $phpVersion, - private string $dateTimeClass = DateTime::class, + private string $dateTimeClass, ) { } 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 index b0eee10181..0c4c0bd9dd 100644 --- a/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php @@ -5,6 +5,7 @@ use DateTimeZone; 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; @@ -14,7 +15,8 @@ use PHPStan\Type\TypeCombinator; use function count; -class DateTimeZoneConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +#[AutowiredService] +final class DateTimeZoneConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { public function __construct(private PhpVersion $phpVersion) diff --git a/src/Type/Php/DefineConstantTypeSpecifyingExtension.php b/src/Type/Php/DefineConstantTypeSpecifyingExtension.php index 5f996c55e1..7364ff54e9 100644 --- a/src/Type/Php/DefineConstantTypeSpecifyingExtension.php +++ b/src/Type/Php/DefineConstantTypeSpecifyingExtension.php @@ -9,12 +9,14 @@ 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; @@ -56,9 +58,8 @@ public function specifyTypes( ), $scope->getType($node->getArgs()[1]->value), TypeSpecifierContext::createTruthy(), - true, $scope, - ); + )->setAlwaysOverwriteTypes(); } } diff --git a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php index 5f37995240..df0fe21f2b 100644 --- a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php +++ b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.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\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; @@ -63,7 +65,6 @@ public function specifyTypes( $expr, new MixedType(), $context, - false, $scope, ); } diff --git a/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php b/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php index 0a96b8d73f..e6ae81a8d4 100644 --- a/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ 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; @@ -12,7 +13,8 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class DioStatDynamicFunctionReturnTypeExtension implements 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 index 3075827a96..8ade76245a 100644 --- a/src/Type/Php/DsMapDynamicMethodThrowTypeExtension.php +++ b/src/Type/Php/DsMapDynamicMethodThrowTypeExtension.php @@ -4,19 +4,22 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicMethodThrowTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\VoidType; use function count; +use function in_array; +#[AutowiredService] final class DsMapDynamicMethodThrowTypeExtension implements DynamicMethodThrowTypeExtension { public function isMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getDeclaringClass()->getName() === 'Ds\Map' - && ($methodReflection->getName() === 'get' || $methodReflection->getName() === 'remove'); + && in_array($methodReflection->getName(), ['get', 'remove'], true); } public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type diff --git a/src/Type/Php/DsMapDynamicReturnTypeExtension.php b/src/Type/Php/DsMapDynamicReturnTypeExtension.php index 50ecba6226..6b833656d7 100644 --- a/src/Type/Php/DsMapDynamicReturnTypeExtension.php +++ b/src/Type/Php/DsMapDynamicReturnTypeExtension.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\DynamicMethodReturnTypeExtension; use PHPStan\Type\Type; @@ -11,6 +12,7 @@ use function count; use function in_array; +#[AutowiredService] final class DsMapDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { diff --git a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php index b69b412e58..65cf25e8d5 100644 --- a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php @@ -4,9 +4,12 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -14,6 +17,7 @@ use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\StringType; @@ -22,7 +26,8 @@ use PHPStan\Type\TypeUtils; use function count; -class ExplodeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ExplodeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private PhpVersion $phpVersion) @@ -54,7 +59,22 @@ public function getTypeFromFunctionCall( return new ConstantBooleanType(false); } - $returnType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new StringType())); + $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() diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 7ae801bfd9..44e3fb55b9 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Php; use PhpParser\Node; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; @@ -32,8 +33,8 @@ use function octdec; use function preg_match; use function sprintf; -use const PHP_FLOAT_EPSILON; +#[AutowiredService] final class FilterFunctionReturnTypeHelper { @@ -293,7 +294,7 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp } if ($in instanceof ConstantFloatType) { - return $in->getValue() - (int) $in->getValue() <= PHP_FLOAT_EPSILON + return $in->getValue() - (int) $in->getValue() === 0.0 ? $in->toInteger() : $defaultType; } @@ -398,13 +399,13 @@ private function getOptions(Type $flagsType, int $filterValue): array $optionNames = array_merge(['default'], $this->getFilterTypeOptions()[$filterValue] ?? []); foreach ($optionNames as $optionName) { - $optionaNameType = new ConstantStringType($optionName); - if (!$optionsType->hasOffsetValueType($optionaNameType)->yes()) { + $optionalNameType = new ConstantStringType($optionName); + if (!$optionsType->hasOffsetValueType($optionalNameType)->yes()) { $options[$optionName] = null; continue; } - $options[$optionName] = $optionsType->getOffsetValueType($optionaNameType); + $options[$optionName] = $optionsType->getOffsetValueType($optionalNameType); } return $options; diff --git a/src/Type/Php/FilterInputDynamicReturnTypeExtension.php b/src/Type/Php/FilterInputDynamicReturnTypeExtension.php index 0dd934cd32..d1679bcdda 100644 --- a/src/Type/Php/FilterInputDynamicReturnTypeExtension.php +++ b/src/Type/Php/FilterInputDynamicReturnTypeExtension.php @@ -4,12 +4,14 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use function count; -class FilterInputDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class FilterInputDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper) diff --git a/src/Type/Php/FilterVarArrayDynamicReturnTypeExtension.php b/src/Type/Php/FilterVarArrayDynamicReturnTypeExtension.php index caaca73501..d92e4e5882 100644 --- a/src/Type/Php/FilterVarArrayDynamicReturnTypeExtension.php +++ b/src/Type/Php/FilterVarArrayDynamicReturnTypeExtension.php @@ -5,6 +5,7 @@ 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\AccessoryArrayListType; @@ -26,7 +27,8 @@ use function in_array; use function strtolower; -class FilterVarArrayDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class FilterVarArrayDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper, private ReflectionProvider $reflectionProvider) @@ -88,7 +90,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ); $arrayType = new ArrayType($inputArgType->getIterableKeyType(), $valueType); - return $isList ? AccessoryArrayListType::intersectWith($arrayType) : $arrayType; + return $isList ? TypeCombinator::intersect($arrayType, new AccessoryArrayListType()) : $arrayType; } // Override $add_empty option @@ -116,7 +118,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $addEmpty ? TypeCombinator::addNull($valueType) : $valueType, ); - return $isList ? AccessoryArrayListType::intersectWith($arrayType) : $arrayType; + return $isList ? TypeCombinator::intersect($arrayType, new AccessoryArrayListType()) : $arrayType; } return null; diff --git a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php index 438d6440e9..1aeacdb0cf 100644 --- a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php +++ b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php @@ -4,13 +4,15 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use function count; use function strtolower; -class FilterVarDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class FilterVarDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper) diff --git a/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php index de7e8dfe40..330735d9e4 100644 --- a/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php @@ -11,6 +11,7 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\CallableType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -18,7 +19,8 @@ use PHPStan\Type\FunctionTypeSpecifyingExtension; use function ltrim; -class FunctionExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class FunctionExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -42,7 +44,6 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n ]), new ConstantBooleanType(true), $context, - false, $scope, ); } @@ -51,7 +52,6 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $node->getArgs()[0]->value, new CallableType(), $context, - false, $scope, ); } diff --git a/src/Type/Php/GetCalledClassDynamicReturnTypeExtension.php b/src/Type/Php/GetCalledClassDynamicReturnTypeExtension.php index c570e8ec86..e290bea849 100644 --- a/src/Type/Php/GetCalledClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetCalledClassDynamicReturnTypeExtension.php @@ -6,12 +6,14 @@ 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\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; -class GetCalledClassDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class GetCalledClassDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/GetClassDynamicReturnTypeExtension.php b/src/Type/Php/GetClassDynamicReturnTypeExtension.php index a6c2fdfdb5..14a06d90c0 100644 --- a/src/Type/Php/GetClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetClassDynamicReturnTypeExtension.php @@ -4,6 +4,7 @@ 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; @@ -23,7 +24,8 @@ use PHPStan\Type\UnionType; use function count; -class GetClassDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class GetClassDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php b/src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php index 936f5b5adf..6223240bb1 100644 --- a/src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php +++ b/src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php @@ -2,19 +2,22 @@ namespace PHPStan\Type\Php; -use Closure; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use function array_key_first; use function array_map; use function count; -class GetDebugTypeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class GetDebugTypeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -30,13 +33,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $argType = $scope->getType($functionCall->getArgs()[0]->value); if ($argType instanceof UnionType) { - return new UnionType(array_map(Closure::fromCallable([self::class, 'resolveOneType']), $argType->getTypes())); + 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 { @@ -71,7 +75,16 @@ private static function resolveOneType(Type $type): Type } if ($reflection->isAnonymous()) { // phpcs:ignore - $types[] = new ConstantStringType('class@anonymous'); + $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'); + } } } @@ -81,7 +94,7 @@ private static function resolveOneType(Type $type): Type case 1: return $types[0]; default: - return new UnionType($types); + return TypeCombinator::union(...$types); } } 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 3dc2dbba5b..e4db21dda9 100644 --- a/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ReflectionProvider; @@ -18,7 +19,8 @@ use function array_map; use function count; -class GetParentClassDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class GetParentClassDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private ReflectionProvider $reflectionProvider) diff --git a/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php b/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php index eb396ad797..757ffdf238 100644 --- a/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ 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; @@ -15,7 +16,8 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; -class GettimeofdayDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class GettimeofdayDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/GettypeFunctionReturnTypeExtension.php b/src/Type/Php/GettypeFunctionReturnTypeExtension.php index 566faa3243..79c1c42124 100644 --- a/src/Type/Php/GettypeFunctionReturnTypeExtension.php +++ b/src/Type/Php/GettypeFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -15,7 +16,8 @@ use PHPStan\Type\UnionType; use function count; -class GettypeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class GettypeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/HashFunctionsReturnTypeExtension.php b/src/Type/Php/HashFunctionsReturnTypeExtension.php index 72b26d3dd5..7798d8f0e8 100644 --- a/src/Type/Php/HashFunctionsReturnTypeExtension.php +++ b/src/Type/Php/HashFunctionsReturnTypeExtension.php @@ -4,25 +4,28 @@ 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\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; -use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use function array_map; +use function count; use function hash_algos; use function in_array; +use function is_bool; use function strtolower; +#[AutowiredService] final class HashFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -30,26 +33,32 @@ final class HashFunctionsReturnTypeExtension implements DynamicFunctionReturnTyp 'hash' => [ 'cryptographic' => false, 'possiblyFalse' => false, + 'binary' => 2, ], 'hash_file' => [ 'cryptographic' => false, 'possiblyFalse' => true, + 'binary' => 2, ], 'hash_hkdf' => [ 'cryptographic' => true, 'possiblyFalse' => false, + 'binary' => true, ], 'hash_hmac' => [ 'cryptographic' => true, 'possiblyFalse' => false, + 'binary' => 3, ], 'hash_hmac_file' => [ 'cryptographic' => true, 'possiblyFalse' => true, + 'binary' => 3, ], 'hash_pbkdf2' => [ 'cryptographic' => true, 'possiblyFalse' => false, + 'binary' => 5, ], ]; @@ -86,37 +95,46 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return isset(self::SUPPORTED_FUNCTIONS[$name]); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (!isset($functionCall->getArgs()[0])) { - return $defaultReturnType; + return null; } - $algorithmType = $scope->getType($functionCall->getArgs()[0]->value); - if ($algorithmType instanceof MixedType) { - return TypeUtils::toBenevolentUnion($defaultReturnType); + $functionData = self::SUPPORTED_FUNCTIONS[strtolower($functionReflection->getName())]; + if (is_bool($functionData['binary'])) { + $binaryType = new ConstantBooleanType($functionData['binary']); + } elseif (isset($functionCall->getArgs()[$functionData['binary']])) { + $binaryType = $scope->getType($functionCall->getArgs()[$functionData['binary']]->value); + } else { + $binaryType = new ConstantBooleanType(false); + } + + $stringTypes = [ + new StringType(), + new AccessoryNonFalsyStringType(), + ]; + if ($binaryType->isFalse()->yes()) { + $stringTypes[] = new AccessoryLowercaseStringType(); } + $stringReturnType = new IntersectionType($stringTypes); + $algorithmType = $scope->getType($functionCall->getArgs()[0]->value); $constantAlgorithmTypes = $algorithmType->getConstantStrings(); + if (count($constantAlgorithmTypes) === 0) { + if ($functionData['possiblyFalse'] || !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + return TypeUtils::toBenevolentUnion(TypeCombinator::union($stringReturnType, new ConstantBooleanType(false))); + } - if ($constantAlgorithmTypes === []) { - return TypeUtils::toBenevolentUnion($defaultReturnType); + return $stringReturnType; } $neverType = new NeverType(); $falseType = new ConstantBooleanType(false); - $nonEmptyString = new IntersectionType([ - new StringType(), - new AccessoryNonEmptyStringType(), - ]); - $invalidAlgorithmType = $this->phpVersion->throwsValueErrorForInternalFunctions() ? $neverType : $falseType; - $functionData = self::SUPPORTED_FUNCTIONS[strtolower($functionReflection->getName())]; $returnTypes = array_map( - function (ConstantStringType $type) use ($functionData, $nonEmptyString, $invalidAlgorithmType) { + function (ConstantStringType $type) use ($functionData, $stringReturnType, $invalidAlgorithmType) { $algorithm = strtolower($type->getValue()); if (!in_array($algorithm, $this->hashAlgorithms, true)) { return $invalidAlgorithmType; @@ -124,7 +142,7 @@ function (ConstantStringType $type) use ($functionData, $nonEmptyString, $invali if ($functionData['cryptographic'] && in_array($algorithm, self::NON_CRYPTOGRAPHIC_ALGORITHMS, true)) { return $invalidAlgorithmType; } - return $nonEmptyString; + return $stringReturnType; }, $constantAlgorithmTypes, ); diff --git a/src/Type/Php/HighlightStringDynamicReturnTypeExtension.php b/src/Type/Php/HighlightStringDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..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 47dc50fee9..5b4a28b2ec 100644 --- a/src/Type/Php/HrtimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/HrtimeFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ 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; @@ -16,7 +17,8 @@ use PHPStan\Type\TypeUtils; use function count; -class HrtimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class HrtimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -26,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], [], TrinaryLogic::createYes()); + $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) { diff --git a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php index 0daed61c52..699b21c09c 100644 --- a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php +++ b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php @@ -4,13 +4,17 @@ 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\ConstantScalarType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; @@ -20,7 +24,8 @@ use function implode; use function in_array; -class ImplodeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class ImplodeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -78,10 +83,11 @@ private function implode(Type $arrayType, Type $separatorType): Type } $accessoryTypes = []; + $valueTypeAsString = $arrayType->getIterableValueType()->toString(); if ($arrayType->isIterableAtLeastOnce()->yes()) { - if ($arrayType->getIterableValueType()->isNonFalsyString()->yes() || $separatorType->isNonFalsyString()->yes()) { + if ($valueTypeAsString->isNonFalsyString()->yes() || $separatorType->isNonFalsyString()->yes()) { $accessoryTypes[] = new AccessoryNonFalsyStringType(); - } elseif ($arrayType->getIterableValueType()->isNonEmptyString()->yes() || $separatorType->isNonEmptyString()->yes()) { + } elseif ($valueTypeAsString->isNonEmptyString()->yes() || $separatorType->isNonEmptyString()->yes()) { $accessoryTypes[] = new AccessoryNonEmptyStringType(); } } @@ -90,6 +96,12 @@ private function implode(Type $arrayType, Type $separatorType): Type 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(); @@ -106,14 +118,28 @@ private function inferConstantType(ConstantArrayType $arrayType, ConstantStringT $valueTypes = $array->getValueTypes(); $arrayValues = []; + $combinationsCount = 1; foreach ($valueTypes as $valueType) { - if (!$valueType instanceof ConstantScalarType) { + $constScalars = $valueType->getConstantScalarValues(); + if (count($constScalars) === 0) { return null; } - $arrayValues[] = $valueType->getValue(); + $arrayValues[] = $constScalars; + $combinationsCount *= count($constScalars); + } + + if ($combinationsCount > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return null; } - $strings[] = new ConstantStringType(implode($separatorType->getValue(), $arrayValues)); + $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 3bf0ecfbb5..c26d550049 100644 --- a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php @@ -11,6 +11,7 @@ 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\Accessory\NonEmptyArrayType; @@ -18,11 +19,11 @@ use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\MixedType; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; use function strtolower; -class InArrayFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class InArrayFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -60,23 +61,24 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $isStrictComparison = $isStrictComparison || $needleType->isEnum()->yes() - || $arrayValueType->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 === null) { - continue; - } if ($item->unpack) { $types = null; break; } if ($isStrictComparison) { - $itemTypes = $this->typeSpecifier->resolveIdentical(new Identical($needleExpr, $item->value), $scope, $context, null); + $itemTypes = $this->typeSpecifier->resolveIdentical(new Identical($needleExpr, $item->value), $scope, $context); } else { - $itemTypes = $this->typeSpecifier->resolveEqual(new Equal($needleExpr, $item->value), $scope, $context, null); + $itemTypes = $this->typeSpecifier->resolveEqual(new Equal($needleExpr, $item->value), $scope, $context); } if ($types === null) { @@ -102,7 +104,6 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $node->getArgs()[1]->value, TypeCombinator::intersect($arrayType, new NonEmptyArrayType()), $context, - false, $scope, ); } @@ -115,17 +116,15 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $context->true() || ( $context->false() - && ( - count(TypeUtils::getConstantScalars($arrayValueType)) > 0 - || count(TypeUtils::getEnumCaseObjects($arrayValueType)) > 0 - ) + && count($arrayValueType->getFiniteTypes()) > 0 + && count($needleType->getFiniteTypes()) > 0 + && $arrayType->isIterableAtLeastOnce()->yes() ) ) { $specifiedTypes = $this->typeSpecifier->create( $needleExpr, $arrayValueType, $context, - false, $scope, ); if ($needleExpr instanceof AlwaysRememberedExpr) { @@ -133,7 +132,6 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $needleExpr->getExpr(), $arrayValueType, $context, - false, $scope, )); } @@ -143,10 +141,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $context->true() || ( $context->false() - && ( - count(TypeUtils::getConstantScalars($needleType)) === 1 - || count(TypeUtils::getEnumCaseObjects($needleType)) === 1 - ) + && count($needleType->getFiniteTypes()) === 1 ) ) { if ($context->true()) { @@ -159,7 +154,6 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $node->getArgs()[1]->value, new ArrayType(new MixedType(), $arrayValueType), TypeSpecifierContext::createTrue(), - false, $scope, )); } @@ -169,7 +163,6 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $node->getArgs()[1]->value, TypeCombinator::intersect($arrayType, new NonEmptyArrayType()), $context, - false, $scope, )); } diff --git a/src/Type/Php/IniGetReturnTypeExtension.php b/src/Type/Php/IniGetReturnTypeExtension.php index 4e4de99bc4..21aab0b82f 100644 --- a/src/Type/Php/IniGetReturnTypeExtension.php +++ b/src/Type/Php/IniGetReturnTypeExtension.php @@ -4,6 +4,7 @@ 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; @@ -13,7 +14,8 @@ use function array_key_exists; use function count; -class IniGetReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class IniGetReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/IntdivThrowTypeExtension.php b/src/Type/Php/IntdivThrowTypeExtension.php index ef8ac1ef54..baab09248b 100644 --- a/src/Type/Php/IntdivThrowTypeExtension.php +++ b/src/Type/Php/IntdivThrowTypeExtension.php @@ -6,6 +6,7 @@ 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; @@ -14,7 +15,8 @@ use function count; use const PHP_INT_MIN; -class IntdivThrowTypeExtension implements DynamicFunctionThrowTypeExtension +#[AutowiredService] +final class IntdivThrowTypeExtension implements DynamicFunctionThrowTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php index 0eb26199df..5d19e4950f 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php @@ -8,6 +8,7 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; @@ -15,7 +16,8 @@ use function count; use function strtolower; -class IsAFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class IsAFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -47,11 +49,17 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $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 $this->typeSpecifier->create( $node->getArgs()[0]->value, - $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, true), + $resultType, $context, - false, $scope, ); } diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php index d1f4a1bd82..df33262d8d 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Php; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericClassStringType; @@ -16,6 +17,7 @@ use function array_unique; use function array_values; +#[AutowiredService] final class IsAFunctionTypeSpecifyingHelper { diff --git a/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php index d05b999422..0beb301d46 100644 --- a/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php @@ -8,6 +8,7 @@ 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; @@ -15,15 +16,12 @@ use PHPStan\Type\MixedType; use function strtolower; -class IsArrayFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class IsArrayFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; - public function __construct(private bool $explicitMixed) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { return strtolower($functionReflection->getName()) === 'is_array' @@ -39,7 +37,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n throw new ShouldNotHappenException(); } - return $this->typeSpecifier->create($node->getArgs()[0]->value, new ArrayType(new MixedType($this->explicitMixed), new MixedType($this->explicitMixed)), $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/IsCallableFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php index 38bb1f3044..a0cde666e5 100644 --- a/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php @@ -11,6 +11,7 @@ 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; @@ -18,7 +19,8 @@ use function count; use function strtolower; -class IsCallableFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class IsCallableFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -51,10 +53,6 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n && $valueType->isConstantArray()->yes() && !$valueType->isCallable()->no() ) { - if ($value->items[0] === null || $value->items[1] === null) { - throw new ShouldNotHappenException(); - } - $functionCall = new FuncCall(new Name('method_exists'), [ new Arg($value->items[0]->value), new Arg($value->items[1]->value), @@ -62,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/IsIterableFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php index 470d196894..475f9a83cb 100644 --- a/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php @@ -8,6 +8,7 @@ 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; @@ -15,7 +16,8 @@ use PHPStan\Type\MixedType; use function strtolower; -class IsIterableFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class IsIterableFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -36,7 +38,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n return new SpecifiedTypes(); } - return $this->typeSpecifier->create($node->getArgs()[0]->value, new IterableType(new MixedType(), new MixedType()), $context, false, $scope); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new IterableType(new MixedType(), new MixedType()), $context, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php index 3864db74e5..e910bd5ea1 100644 --- a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php @@ -8,6 +8,7 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\FunctionTypeSpecifyingExtension; @@ -15,7 +16,8 @@ use function count; use function strtolower; -class IsSubclassOfFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class IsSubclassOfFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -48,11 +50,17 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n return new SpecifiedTypes([], []); } + $resultType = $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, false); + + // prevent false-positives in IsAFunctionTypeSpecifyingHelper + if ($classType->getConstantStrings() === [] && $resultType->isSuperTypeOf($objectOrClassType)->yes()) { + return new SpecifiedTypes([], []); + } + return $this->typeSpecifier->create( $node->getArgs()[0]->value, - $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, false), + $resultType, $context, - false, $scope, ); } diff --git a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php index 3eba789175..b4ed3b2d18 100644 --- a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php +++ b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php @@ -4,14 +4,19 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\ErrorType; use PHPStan\Type\IntegerType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function strtolower; +#[AutowiredService] final class IteratorToArrayFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -29,28 +34,28 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $traversableType = $scope->getType($arguments[0]->value); - $arrayKeyType = $traversableType->getIterableKeyType(); - $isList = false; if (isset($arguments[1])) { $preserveKeysType = $scope->getType($arguments[1]->value); if ($preserveKeysType->isFalse()->yes()) { - $arrayKeyType = new IntegerType(); - $isList = true; + return TypeCombinator::intersect(new ArrayType( + new IntegerType(), + $traversableType->getIterableValueType(), + ), new AccessoryArrayListType()); } } - $arrayType = new ArrayType( - $arrayKeyType, - $traversableType->getIterableValueType(), - ); + $arrayKeyType = $traversableType->getIterableKeyType()->toArrayKey(); - if ($isList) { - $arrayType = AccessoryArrayListType::intersectWith($arrayType); + if ($arrayKeyType instanceof ErrorType) { + return new NeverType(true); } - return $arrayType; + return new ArrayType( + $arrayKeyType, + $traversableType->getIterableValueType(), + ); } } diff --git a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php index a06e2b3370..f9e551cc8a 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -5,6 +5,7 @@ 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; @@ -20,7 +21,8 @@ use function is_bool; use function json_decode; -class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { /** @var array */ @@ -54,7 +56,11 @@ public function getTypeFromFunctionCall( ): Type { $argumentPosition = $this->argumentPositions[$functionReflection->getName()]; - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); if ($functionReflection->getName() === 'json_decode') { $defaultReturnType = $this->narrowTypeForJsonDecode($functionCall, $scope, $defaultReturnType); diff --git a/src/Type/Php/JsonThrowTypeExtension.php b/src/Type/Php/JsonThrowTypeExtension.php index 29eaf4f290..19cfcbaaea 100644 --- a/src/Type/Php/JsonThrowTypeExtension.php +++ b/src/Type/Php/JsonThrowTypeExtension.php @@ -5,6 +5,7 @@ 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\BitwiseFlagHelper; @@ -13,7 +14,8 @@ use PHPStan\Type\Type; use function in_array; -class JsonThrowTypeExtension implements DynamicFunctionThrowTypeExtension +#[AutowiredService] +final class JsonThrowTypeExtension implements DynamicFunctionThrowTypeExtension { private const ARGUMENTS_POSITIONS = [ diff --git a/src/Type/Php/LtrimFunctionReturnTypeExtension.php b/src/Type/Php/LtrimFunctionReturnTypeExtension.php index 964d605b27..2db0c7be4e 100644 --- a/src/Type/Php/LtrimFunctionReturnTypeExtension.php +++ b/src/Type/Php/LtrimFunctionReturnTypeExtension.php @@ -4,15 +4,24 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\ClassStringType; 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 ltrim; +use function preg_match; -class LtrimFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class LtrimFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -22,22 +31,57 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - if (count($functionCall->getArgs()) !== 2) { + 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); - if ($trimChars instanceof ConstantStringType && $trimChars->getValue() === '\\' && $string->isClassStringType()->yes()) { - if ($string instanceof ConstantStringType) { - return new ConstantStringType(ltrim($string->getValue(), $trimChars->getValue()), true); + $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 new ClassStringType(); + return TypeCombinator::union(...$result); } - return null; + return $defaultType; } } diff --git a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php index ae3aa752f9..f567eadfb6 100644 --- a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php +++ b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php @@ -4,16 +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\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntegerType; +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; -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'; @@ -30,16 +45,93 @@ public function getTypeFromFunctionCall( } $argType = $scope->getType($functionCall->getArgs()[0]->value); - $isString = $argType->isString(); - $isArray = $argType->isArray(); - $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(), ',')) { + $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 null; + $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 13f8d238b0..596e2ed8c1 100644 --- a/src/Type/Php/MbFunctionsReturnTypeExtension.php +++ b/src/Type/Php/MbFunctionsReturnTypeExtension.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\Reflection\ParametersAcceptorSelector; @@ -21,7 +22,8 @@ use function array_unique; use function count; -class MbFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class MbFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { use MbFunctionsReturnTypeExtensionTrait; @@ -47,7 +49,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo 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) { diff --git a/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php b/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php index cb246f7e3d..48d6b5e712 100644 --- a/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php +++ b/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php @@ -4,11 +4,11 @@ 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\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; @@ -33,7 +33,8 @@ use function sprintf; use function var_export; -class MbStrlenFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class MbStrlenFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { private const UNSUPPORTED_ENCODING = 'unsupported'; @@ -93,33 +94,20 @@ public function getTypeFromFunctionCall( } $argType = $scope->getType($args[0]->value); - - if ($argType->isSuperTypeOf(new BooleanType())->yes()) { - $constantScalars = TypeCombinator::remove($argType, new BooleanType())->getConstantScalarTypes(); - if (count($constantScalars) > 0) { - $constantScalars[] = new ConstantBooleanType(true); - $constantScalars[] = new ConstantBooleanType(false); - } - } else { - $constantScalars = $argType->getConstantScalarTypes(); - } + $constantScalars = $argType->getConstantScalarValues(); $lengths = []; foreach ($constantScalars as $constantScalar) { - $stringScalar = $constantScalar->toString(); - if (!($stringScalar instanceof ConstantStringType)) { - $lengths = []; - break; - } + $stringScalar = (string) $constantScalar; foreach ($encodings as $encoding) { if (!$this->isSupportedEncoding($encoding)) { continue; } - $length = @mb_strlen($stringScalar->getValue(), $encoding); + $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->getValue(), true))); + throw new ShouldNotHappenException(sprintf('Got false on a supported encoding %s and value %s', $encoding, var_export($stringScalar, true))); } $lengths[] = $length; } @@ -147,7 +135,11 @@ public function getTypeFromFunctionCall( $range = new ConstantIntegerType(0); } else { $range = TypeCombinator::remove( - ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(), + ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(), new ConstantBooleanType(false), ); } diff --git a/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php b/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php index f5d0055d9b..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; @@ -18,7 +19,8 @@ use function in_array; use function strtolower; -class MbSubstituteCharacterDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class MbSubstituteCharacterDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private PhpVersion $phpVersion) diff --git a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php index 08a366a59b..3db54618e5 100644 --- a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php @@ -3,14 +3,17 @@ 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; @@ -18,7 +21,8 @@ use PHPStan\Type\UnionType; use function count; -class MethodExistsTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class MethodExistsTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -48,12 +52,17 @@ public function specifyTypes( { $methodNameType = $scope->getType($node->getArgs()[1]->value); if (!$methodNameType instanceof ConstantStringType) { - return new SpecifiedTypes([], []); + 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->isString()->yes()) { - if ($objectType->isClassStringType()->yes()) { + if ($objectType->isClassString()->yes()) { return $this->typeSpecifier->create( $node->getArgs()[0]->value, new IntersectionType([ @@ -61,7 +70,6 @@ public function specifyTypes( new HasMethodType($methodNameType->getValue()), ]), $context, - false, $scope, ); } @@ -79,7 +87,6 @@ public function specifyTypes( new ClassStringType(), ]), $context, - false, $scope, ); } diff --git a/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php b/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php index 422e1355c6..63ab8b2669 100644 --- a/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ 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; @@ -14,7 +15,8 @@ use PHPStan\Type\UnionType; use function count; -class MicrotimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class MicrotimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/MinMaxFunctionReturnTypeExtension.php b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php index e1633c0b64..26864a3054 100644 --- a/src/Type/Php/MinMaxFunctionReturnTypeExtension.php +++ b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php @@ -6,12 +6,13 @@ 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\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; @@ -20,7 +21,8 @@ use function count; use function in_array; -class MinMaxFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class MinMaxFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct( @@ -61,15 +63,20 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $argType1 = $scope->getType($args[1]->value); if ($argType0->isArray()->no() && $argType1->isArray()->no()) { + $comparisonExpr = new Smaller( + new AlwaysRememberedExpr($args[0]->value, $argType0, $scope->getNativeType($args[0]->value)), + new AlwaysRememberedExpr($args[1]->value, $argType1, $scope->getNativeType($args[1]->value)), + ); + if ($functionName === 'min') { return $scope->getType(new Ternary( - new Smaller($args[0]->value, $args[1]->value), + $comparisonExpr, $args[0]->value, $args[1]->value, )); } elseif ($functionName === 'max') { return $scope->getType(new Ternary( - new Smaller($args[0]->value, $args[1]->value), + $comparisonExpr, $args[1]->value, $args[0]->value, )); @@ -152,16 +159,16 @@ private function processType( { $resultType = null; foreach ($types as $type) { - if (!$type instanceof ConstantType) { - return TypeCombinator::union(...$types); - } - if ($resultType === null) { $resultType = $type; continue; } $compareResult = $this->compareTypes($resultType, $type); + if ($compareResult === null) { + return TypeCombinator::union(...$types); + } + if ($functionName === 'min') { if ($compareResult === $type) { $resultType = $type; @@ -186,15 +193,15 @@ private function compareTypes( ): ?Type { if ( - $firstType->isConstantArray()->yes() - && $secondType instanceof ConstantScalarType + $firstType->isArray()->yes() + && $secondType->isConstantScalarValue()->yes() ) { return $secondType; } if ( - $firstType instanceof ConstantScalarType - && $secondType->isConstantArray()->yes() + $firstType->isConstantScalarValue()->yes() + && $secondType->isArray()->yes() ) { return $firstType; } diff --git a/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php b/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php index cb7cf0eb07..913f97a10c 100644 --- a/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php +++ b/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php @@ -2,19 +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\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 @@ -45,6 +50,15 @@ public function getTypeFromFunctionCall( 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([ @@ -62,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 1a564b358c..8da8eda986 100644 --- a/src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php @@ -4,6 +4,7 @@ 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; @@ -14,6 +15,7 @@ use PHPStan\Type\Type; use function in_array; +#[AutowiredService] final class NumberFormatFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { 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/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 2a3d43c9c0..66035119d3 100644 --- a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php @@ -4,14 +4,17 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; 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\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\NullType; use PHPStan\Type\StringType; use PHPStan\Type\Type; @@ -28,6 +31,7 @@ use const PHP_URL_SCHEME; use const PHP_URL_USER; +#[AutowiredService] final class ParseUrlFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -37,8 +41,16 @@ 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'; @@ -52,23 +64,22 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $this->cacheReturnTypes(); + $urlType = $scope->getType($functionCall->getArgs()[0]->value); if (count($functionCall->getArgs()) > 1) { $componentType = $scope->getType($functionCall->getArgs()[1]->value); if (!$componentType->isConstantValue()->yes()) { - return $this->createAllComponentsReturnType(); + return $this->createAllComponentsReturnType($urlType->isLowercaseString()->yes()); } $componentType = $componentType->toInteger(); - if (!$componentType instanceof ConstantIntegerType) { - return $this->createAllComponentsReturnType(); + return $this->createAllComponentsReturnType($urlType->isLowercaseString()->yes()); } } else { $componentType = new ConstantIntegerType(-1); } - $urlType = $scope->getType($functionCall->getArgs()[0]->value); if (count($urlType->getConstantStrings()) > 0) { $types = []; foreach ($urlType->getConstantStrings() as $constantString) { @@ -86,21 +97,44 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($componentType->getValue() === -1) { - return TypeCombinator::union($this->createComponentsArray(), new ConstantBooleanType(false)); + 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(), + $this->createComponentsArray(false), ]; $this->allComponentsTogetherType = TypeCombinator::union(...$returnTypes); @@ -109,19 +143,29 @@ private function createAllComponentsReturnType(): Type return $this->allComponentsTogetherType; } - private function createComponentsArray(): Type + private function createComponentsArray(bool $urlIsLowercase): Type { - $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder = ConstantArrayTypeBuilder::createEmpty(); - if ($this->componentTypesPairedStrings === null) { - throw new ShouldNotHappenException(); - } + if ($urlIsLowercase) { + if ($this->componentTypesPairedStringsForLowercaseString === null) { + throw new ShouldNotHappenException(); + } - foreach ($this->componentTypesPairedStrings as $componentName => $componentValueType) { - $builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType, true); + foreach ($this->componentTypesPairedStringsForLowercaseString as $componentName => $componentValueType) { + $builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType, true); + } + } else { + if ($this->componentTypesPairedStrings === null) { + throw new ShouldNotHappenException(); + } + + foreach ($this->componentTypesPairedStrings as $componentName => $componentValueType) { + $builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType, true); + } } - return $builder->getArray(); + return $builder->getArray(); } private function cacheReturnTypes(): void @@ -131,11 +175,13 @@ private function cacheReturnTypes(): void } $string = new StringType(); + $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); + $lowercaseStringOrFalseOrNull = TypeCombinator::union($lowercaseString, $false, $null); $portOrFalseOrNull = TypeCombinator::union($port, $false, $null); $this->componentTypesPairedConstants = [ @@ -148,6 +194,16 @@ private function cacheReturnTypes(): void 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, @@ -159,6 +215,16 @@ private function cacheReturnTypes(): void '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 11a5f74752..49cb0dffdb 100644 --- a/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; @@ -17,7 +18,8 @@ use function count; use function sprintf; -class PathinfoFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class PathinfoFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private ReflectionProvider $reflectionProvider) diff --git a/src/Type/Php/PowFunctionReturnTypeExtension.php b/src/Type/Php/PowFunctionReturnTypeExtension.php index ea15dae2e8..d2c0ba4830 100644 --- a/src/Type/Php/PowFunctionReturnTypeExtension.php +++ b/src/Type/Php/PowFunctionReturnTypeExtension.php @@ -5,12 +5,14 @@ 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\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use function count; -class PowFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class PowFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/PregFilterFunctionReturnTypeExtension.php b/src/Type/Php/PregFilterFunctionReturnTypeExtension.php index 3e158d1bcc..6d6660d362 100644 --- a/src/Type/Php/PregFilterFunctionReturnTypeExtension.php +++ b/src/Type/Php/PregFilterFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ 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; @@ -15,7 +16,8 @@ use PHPStan\Type\UnionType; use function count; -class PregFilterFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class PregFilterFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -25,7 +27,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $defaultReturn = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $defaultReturn = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); $argsCount = count($functionCall->getArgs()); if ($argsCount < 3) { diff --git a/src/Type/Php/PregMatchParameterOutTypeExtension.php b/src/Type/Php/PregMatchParameterOutTypeExtension.php index 9066351d9f..b8bd415b45 100644 --- a/src/Type/Php/PregMatchParameterOutTypeExtension.php +++ b/src/Type/Php/PregMatchParameterOutTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\TrinaryLogic; @@ -12,6 +13,7 @@ use function in_array; use function strtolower; +#[AutowiredService] final class PregMatchParameterOutTypeExtension implements FunctionParameterOutTypeExtension { diff --git a/src/Type/Php/PregMatchTypeSpecifyingExtension.php b/src/Type/Php/PregMatchTypeSpecifyingExtension.php index 2c7cad49be..399ee9126f 100644 --- a/src/Type/Php/PregMatchTypeSpecifyingExtension.php +++ b/src/Type/Php/PregMatchTypeSpecifyingExtension.php @@ -8,12 +8,14 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\FunctionTypeSpecifyingExtension; use function in_array; use function strtolower; +#[AutowiredService] final class PregMatchTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { @@ -53,10 +55,18 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $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, TrinaryLogic::createFromBoolean($context->true()), $scope); + $matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, $wasMatched, $scope); } else { - $matchedType = $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope); + $matchedType = $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, $wasMatched, $scope); } if ($matchedType === null) { return new SpecifiedTypes(); @@ -68,14 +78,17 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $context = $context->negate(); } - return $this->typeSpecifier->create( + $types = $this->typeSpecifier->create( $matchesArg->value, $matchedType, $context, - $overwrite, $scope, - $node, - ); + )->setRootExpr($node); + if ($overwrite) { + $types = $types->setAlwaysOverwriteTypes(); + } + + return $types; } } diff --git a/src/Type/Php/PregReplaceCallbackClosureTypeExtension.php b/src/Type/Php/PregReplaceCallbackClosureTypeExtension.php index a7ea4dc133..1be24ff645 100644 --- a/src/Type/Php/PregReplaceCallbackClosureTypeExtension.php +++ b/src/Type/Php/PregReplaceCallbackClosureTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; @@ -13,6 +14,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; +#[AutowiredService] final class PregReplaceCallbackClosureTypeExtension implements FunctionParameterClosureTypeExtension { diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 6300d6887d..ec1c814a47 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -2,29 +2,43 @@ namespace PHPStan\Type\Php; +use Nette\Utils\RegexpException; +use Nette\Utils\Strings; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -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\BitwiseFlagHelper; -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\IntegerRangeType; use PHPStan\Type\IntegerType; +use PHPStan\Type\MixedType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +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; -class PregSplitDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class PregSplitDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct( - private BitwiseFlagHelper $bitwiseFlagAnalyser, + private readonly BitwiseFlagHelper $bitwiseFlagAnalyser, ) { } @@ -36,17 +50,164 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo 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 ($flagsArg !== null && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagsArg->value, $scope, 'PREG_SPLIT_OFFSET_CAPTURE')->yes()) { - $type = new ArrayType( - new IntegerType(), - new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new StringType(), IntegerRangeType::fromInterval(0, null)], [2], [], TrinaryLogic::createYes()), + 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(AccessoryArrayListType::intersectWith($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 null; + $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 isValidPattern(string $pattern): bool + { + try { + Strings::match('', $pattern); + } catch (RegexpException) { + return false; + } + return true; + } + + private function isIntOrStringValue(Type $type): bool + { + 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 8907e48a66..e62a04d409 100644 --- a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php @@ -5,21 +5,25 @@ 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 TypeSpecifier $typeSpecifier; @@ -53,6 +57,15 @@ public function specifyTypes( { $propertyNameType = $scope->getType($node->getArgs()[1]->value); if (!$propertyNameType instanceof ConstantStringType) { + return $this->typeSpecifier->create( + new FuncCall(new FullyQualified('property_exists'), $node->getRawArgs()), + new ConstantBooleanType(true), + $context, + $scope, + ); + } + + if ($propertyNameType->getValue() === '') { return new SpecifiedTypes([], []); } @@ -82,7 +95,6 @@ public function specifyTypes( new HasPropertyType($propertyNameType->getValue()), ]), $context, - false, $scope, ); } diff --git a/src/Type/Php/RandomIntFunctionReturnTypeExtension.php b/src/Type/Php/RandomIntFunctionReturnTypeExtension.php index e7e17b90f6..35f978e631 100644 --- a/src/Type/Php/RandomIntFunctionReturnTypeExtension.php +++ b/src/Type/Php/RandomIntFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ 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\DynamicFunctionReturnTypeExtension; @@ -17,7 +18,8 @@ use function max; use function min; -class RandomIntFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class RandomIntFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/RangeFunctionReturnTypeExtension.php b/src/Type/Php/RangeFunctionReturnTypeExtension.php index 8a67f1d6ab..de6e43bd79 100644 --- a/src/Type/Php/RangeFunctionReturnTypeExtension.php +++ b/src/Type/Php/RangeFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -27,7 +28,8 @@ use function is_array; use function range; -class RangeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class RangeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { private const RANGE_LENGTH_THRESHOLD = 50; @@ -68,7 +70,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } try { - $rangeValues = range($startConstant->getValue(), $endConstant->getValue(), $stepConstant->getValue()); + $rangeValues = @range($startConstant->getValue(), $endConstant->getValue(), $stepConstant->getValue()); } catch (ValueError) { continue; } @@ -79,31 +81,49 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if (count($rangeValues) > self::RANGE_LENGTH_THRESHOLD) { - if ($startConstant instanceof ConstantIntegerType && $endConstant instanceof ConstantIntegerType) { + if ( + $startConstant instanceof ConstantIntegerType + && $endConstant instanceof ConstantIntegerType + && $stepConstant instanceof ConstantIntegerType + ) { if ($startConstant->getValue() > $endConstant->getValue()) { $tmp = $startConstant; $startConstant = $endConstant; $endConstant = $tmp; } - return AccessoryArrayListType::intersectWith(TypeCombinator::intersect( + return TypeCombinator::intersect( new ArrayType( new IntegerType(), IntegerRangeType::fromInterval($startConstant->getValue(), $endConstant->getValue()), ), new NonEmptyArrayType(), - )); + new AccessoryArrayListType(), + ); } - return AccessoryArrayListType::intersectWith(TypeCombinator::intersect( + if ($stepType->isFloat()->yes()) { + return TypeCombinator::intersect( + new ArrayType( + new IntegerType(), + new FloatType(), + ), + new NonEmptyArrayType(), + new AccessoryArrayListType(), + ); + } + + return TypeCombinator::intersect( new ArrayType( new IntegerType(), TypeCombinator::union( $startConstant->generalize(GeneralizePrecision::moreSpecific()), $endConstant->generalize(GeneralizePrecision::moreSpecific()), + $stepType->generalize(GeneralizePrecision::moreSpecific()), ), ), new NonEmptyArrayType(), - )); + new AccessoryArrayListType(), + ); } $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach ($rangeValues as $value) { @@ -125,30 +145,30 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($isInteger && $isStepInteger) { if ($argType instanceof IntegerRangeType) { - return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $argType)); + return TypeCombinator::intersect(new ArrayType(new IntegerType(), $argType), new AccessoryArrayListType()); } - return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new IntegerType())); + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new IntegerType()), new AccessoryArrayListType()); } if ($argType->isFloat()->yes()) { - return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new FloatType())); + 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 AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $numberType)); + return TypeCombinator::intersect(new ArrayType(new IntegerType(), $numberType), new AccessoryArrayListType()); } if ($argType->isString()->yes()) { - return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new StringType())); + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new StringType()), new AccessoryArrayListType()); } - return AccessoryArrayListType::intersectWith(new ArrayType( + 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 bbe3c9de62..94f0d95379 100644 --- a/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\ClassStringType; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; @@ -13,7 +14,8 @@ use ReflectionClass; use function count; -class ReflectionClassConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +#[AutowiredService] +final class ReflectionClassConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { public function isStaticMethodSupported(MethodReflection $methodReflection): bool diff --git a/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php b/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php index 842821b1e2..2d830e1501 100644 --- a/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php +++ b/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php @@ -8,14 +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; @@ -34,25 +36,38 @@ public function isMethodSupported(MethodReflection $methodReflection, MethodCall { return $methodReflection->getName() === 'isSubclassOf' && isset($node->getArgs()[0]) - && $context->true(); + && !$context->null(); } public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { + $calledOnType = $scope->getType($node->var); + $reflectionType = $calledOnType->getTemplateType(ReflectionClass::class, 'T'); + if (!(new ObjectWithoutClassType())->isSuperTypeOf($reflectionType)->yes()) { + return new SpecifiedTypes(); + } + $valueType = $scope->getType($node->getArgs()[0]->value); - if (!$valueType instanceof ConstantStringType) { - return new SpecifiedTypes([], []); + $objectType = $valueType->getClassStringObjectType(); + + $intersected = TypeCombinator::intersect($reflectionType, $objectType); + $narrowingType = new GenericObjectType(ReflectionClass::class, [$intersected]); + + if ($reflectionType->isSuperTypeOf($objectType)->no()) { + return $this->typeSpecifier->create( + $node->var, + $narrowingType, + $context, + $scope, + ); } return $this->typeSpecifier->create( $node->var, - new GenericObjectType(ReflectionClass::class, [ - new ObjectType($valueType->getValue()), - ]), + $narrowingType, $context, - false, $scope, - ); + )->setAlwaysOverwriteTypes(); } } diff --git a/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php index 56478c55c9..01fe08f354 100644 --- a/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php @@ -5,6 +5,7 @@ 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; @@ -14,7 +15,8 @@ use ReflectionFunction; use function count; -class ReflectionFunctionConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +#[AutowiredService] +final class ReflectionFunctionConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { public function __construct(private ReflectionProvider $reflectionProvider) diff --git a/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php b/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php index bb11536c1b..493ec0e9c3 100644 --- a/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php +++ b/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php @@ -5,15 +5,17 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Generic\GenericObjectType; -use PHPStan\Type\MixedType; +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 { /** @@ -42,7 +44,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method $argType = $scope->getType($methodCall->getArgs()[0]->value); $classType = $argType->getClassStringObjectType(); - return new ArrayType(new MixedType(), new GenericObjectType(ReflectionAttribute::class, [$classType])); + 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 e6aa5823f5..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; @@ -16,7 +17,8 @@ use ReflectionMethod; use function count; -class ReflectionMethodConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +#[AutowiredService] +final class ReflectionMethodConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { public function __construct(private ReflectionProvider $reflectionProvider) diff --git a/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php index 098b3f1567..22140eba68 100644 --- a/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.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\DynamicStaticMethodThrowTypeExtension; @@ -13,7 +14,8 @@ use ReflectionProperty; use function count; -class ReflectionPropertyConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +#[AutowiredService] +final class ReflectionPropertyConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { public function __construct(private ReflectionProvider $reflectionProvider) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 628bcb0d3c..11c3087222 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -4,25 +4,24 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\NullType; -use PHPStan\Type\Regex\RegexAlternation; use PHPStan\Type\Regex\RegexCapturingGroup; use PHPStan\Type\Regex\RegexExpressionHelper; +use PHPStan\Type\Regex\RegexGroupList; use PHPStan\Type\Regex\RegexGroupParser; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use function array_reverse; use function count; use function in_array; use function is_string; @@ -34,6 +33,7 @@ /** * @api */ +#[AutowiredService] final class RegexArrayShapeMatcher { @@ -60,18 +60,10 @@ public function matchExpr(Expr $patternExpr, ?Type $flagsType, TrinaryLogic $was return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched, false); } - /** - * @deprecated use matchExpr() instead for a more precise result - */ - public function matchType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched): ?Type - { - return $this->matchPatternType($patternType, $flagsType, $wasMatched, false); - } - private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched, bool $matchesAll): ?Type { if ($wasMatched->no()) { - return new ConstantArrayType([], []); + return ConstantArrayTypeBuilder::createEmpty()->getArray(); } $constantStrings = $patternType->getConstantStrings(); @@ -116,23 +108,22 @@ private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLo */ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched, bool $matchesAll): ?Type { - $parseResult = $this->regexGroupParser->parseGroups($regex); - if ($parseResult === null) { + $astWalkResult = $this->regexGroupParser->parseGroups($regex); + if ($astWalkResult === null) { // regex could not be parsed by Hoa/Regex return null; } - [$groupList, $markVerbs] = $parseResult; - - $trailingOptionals = 0; - foreach (array_reverse($groupList) as $captureGroup) { - if (!$captureGroup->isOptional()) { - break; - } - $trailingOptionals++; + $groupList = $astWalkResult->getCapturingGroups(); + $markVerbs = $astWalkResult->getMarkVerbs(); + $subjectBaseType = new StringType(); + if ($wasMatched->yes()) { + $subjectBaseType = $astWalkResult->getSubjectBaseType(); } - $onlyOptionalTopLevelGroup = $this->getOnlyOptionalTopLevelGroup($groupList); - $onlyTopLevelAlternation = $this->getOnlyTopLevelAlternation($groupList); + $regexGroupList = new RegexGroupList($groupList); + $trailingOptionals = $regexGroupList->countTrailingOptionals(); + $onlyOptionalTopLevelGroup = $regexGroupList->getOnlyOptionalTopLevelGroup(); + $onlyTopLevelAlternation = $regexGroupList->getOnlyTopLevelAlternation(); $flags ??= 0; if ( @@ -142,11 +133,11 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched ) { // 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 - - $onlyOptionalTopLevelGroup->forceNonOptional(); + $regexGroupList = $regexGroupList->forceGroupNonOptional($onlyOptionalTopLevelGroup); $combiType = $this->buildArrayType( - $groupList, + $subjectBaseType, + $regexGroupList, $wasMatched, $trailingOptionals, $flags, @@ -156,14 +147,15 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched 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( - new ConstantArrayType([new ConstantIntegerType(0)], [$this->createSubjectValueType($flags, $matchesAll)], [0], [], true), + $builder->getArray(), $combiType, ); } - $onlyOptionalTopLevelGroup->clearOverrides(); - return $combiType; } elseif ( !$matchesAll @@ -176,28 +168,29 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $combiTypes = []; $isOptionalAlternation = false; foreach ($onlyTopLevelAlternation->getGroupCombinations() as $groupCombo) { - $comboList = $groupList; + $comboList = new RegexGroupList($groupList); $beforeCurrentCombo = true; - foreach ($comboList as $groupId => $group) { - if (in_array($groupId, $groupCombo, true)) { + foreach ($comboList as $group) { + if (in_array($group->getId(), $groupCombo, true)) { $isOptionalAlternation = $group->inOptionalAlternation(); - $group->forceNonOptional(); + $comboList = $comboList->forceGroupNonOptional($group); $beforeCurrentCombo = false; } elseif ($beforeCurrentCombo && !$group->resetsGroupCounter()) { - $group->forceNonOptional(); - $group->forceType( + $comboList = $comboList->forceGroupTypeAndNonOptional( + $group, $this->containsUnmatchedAsNull($flags, $matchesAll) ? new NullType() : new ConstantStringType(''), ); } elseif ( $group->getAlternationId() === $onlyTopLevelAlternation->getId() && !$this->containsUnmatchedAsNull($flags, $matchesAll) ) { - unset($comboList[$groupId]); + $comboList = $comboList->removeGroup($group); } } $combiType = $this->buildArrayType( + $subjectBaseType, $comboList, $wasMatched, $trailingOptionals, @@ -207,11 +200,6 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched ); $combiTypes[] = $combiType; - - foreach ($groupCombo as $groupId) { - $group = $comboList[$groupId]; - $group->clearOverrides(); - } } if ( @@ -222,7 +210,10 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched ) ) { // positive match has a subject but not any capturing group - $combiTypes[] = new ConstantArrayType([new ConstantIntegerType(0)], [$this->createSubjectValueType($flags, $matchesAll)], [0], [], true); + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantIntegerType(0), $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll)); + + $combiTypes[] = $builder->getArray(); } return TypeCombinator::union(...$combiTypes); @@ -231,7 +222,8 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched // the general case, which should work in all cases but does not yield the most // precise result possible in some cases return $this->buildArrayType( - $groupList, + $subjectBaseType, + $regexGroupList, $wasMatched, $trailingOptionals, $flags, @@ -241,61 +233,11 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched } /** - * @param array $captureGroups - */ - private function getOnlyOptionalTopLevelGroup(array $captureGroups): ?RegexCapturingGroup - { - $group = null; - foreach ($captureGroups as $captureGroup) { - if (!$captureGroup->isTopLevel()) { - continue; - } - - if (!$captureGroup->isOptional()) { - return null; - } - - if ($group !== null) { - return null; - } - - $group = $captureGroup; - } - - return $group; - } - - /** - * @param array $captureGroups - */ - private function getOnlyTopLevelAlternation(array $captureGroups): ?RegexAlternation - { - $alternation = null; - foreach ($captureGroups as $captureGroup) { - if (!$captureGroup->isTopLevel()) { - continue; - } - - if (!$captureGroup->inAlternation()) { - return null; - } - - if ($alternation === null) { - $alternation = $captureGroup->getAlternation(); - } elseif ($alternation->getId() !== $captureGroup->getAlternation()->getId()) { - return null; - } - } - - return $alternation; - } - - /** - * @param array $captureGroups * @param list $markVerbs */ private function buildArrayType( - array $captureGroups, + Type $subjectBaseType, + RegexGroupList $captureGroups, TrinaryLogic $wasMatched, int $trailingOptionals, int $flags, @@ -303,12 +245,13 @@ private function buildArrayType( 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($flags, $matchesAll), + $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll), $this->isSubjectOptional($wasMatched, $matchesAll), ); @@ -321,6 +264,8 @@ private function buildArrayType( $optional = $this->isGroupOptional($captureGroup, $wasMatched, $flags, $isTrailingOptional, $matchesAll); if ($captureGroup->isNamed()) { + $forceList = false; + $builder->setOffsetValueType( $this->getKeyType($captureGroup->getName()), $groupValueType, @@ -350,16 +295,20 @@ private function buildArrayType( } if ($matchesAll && $this->containsSetOrder($flags)) { - $arrayType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $builder->getArray())); + $arrayType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $builder->getArray()), new AccessoryArrayListType()); if (!$wasMatched->yes()) { $arrayType = TypeCombinator::union( - new ConstantArrayType([], []), + ConstantArrayTypeBuilder::createEmpty()->getArray(), $arrayType, ); } return $arrayType; } + if ($forceList) { + return TypeCombinator::intersect($builder->getArray(), new AccessoryArrayListType()); + } + return $builder->getArray(); } @@ -372,13 +321,21 @@ private function isSubjectOptional(TrinaryLogic $wasMatched, bool $matchesAll): return !$wasMatched->yes(); } - private function createSubjectValueType(int $flags, bool $matchesAll): Type + /** + * @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(new StringType(), $flags, $matchesAll)); + $subjectValueType = TypeCombinator::removeNull($this->getValueType($baseType, $flags, $matchesAll)); if ($matchesAll) { + $subjectValueType = TypeCombinator::removeNull($this->getValueType(new StringType(), $flags, $matchesAll)); + if ($this->containsPatternOrder($flags)) { - $subjectValueType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $subjectValueType)); + $subjectValueType = TypeCombinator::intersect( + new ArrayType(new IntegerType(), $subjectValueType), + new AccessoryArrayListType(), + ); } } @@ -413,7 +370,20 @@ private function isGroupOptional(RegexCapturingGroup $captureGroup, TrinaryLogic 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()) { + 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, @@ -429,7 +399,7 @@ private function createGroupValueType(RegexCapturingGroup $captureGroup, Trinary } if ($this->containsPatternOrder($flags)) { - $groupValueType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $groupValueType)); + $groupValueType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $groupValueType), new AccessoryArrayListType()); } return $groupValueType; diff --git a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php index 797ee59bb4..2885b42e68 100644 --- a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php @@ -4,10 +4,14 @@ 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\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; @@ -17,8 +21,10 @@ 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 { private const FUNCTIONS_SUBJECT_POSITION = [ @@ -80,17 +86,33 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( return TypeUtils::toBenevolentUnion($defaultReturnType); } - if ($subjectArgumentType->isNonEmptyString()->yes() && array_key_exists($functionReflection->getName(), self::FUNCTIONS_REPLACE_POSITION)) { + if (array_key_exists($functionReflection->getName(), self::FUNCTIONS_REPLACE_POSITION)) { $replaceArgumentPosition = self::FUNCTIONS_REPLACE_POSITION[$functionReflection->getName()]; if (count($functionCall->getArgs()) > $replaceArgumentPosition) { $replaceArgumentType = $scope->getType($functionCall->getArgs()[$replaceArgumentPosition]->value); + if ($replaceArgumentType->isArray()->yes()) { + $replaceArgumentType = $replaceArgumentType->getIterableValueType(); + } + $accessories = []; if ($subjectArgumentType->isNonFalsyString()->yes() && $replaceArgumentType->isNonFalsyString()->yes()) { - return new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]); + $accessories[] = new AccessoryNonFalsyStringType(); + } elseif ($subjectArgumentType->isNonEmptyString()->yes() && $replaceArgumentType->isNonEmptyString()->yes()) { + $accessories[] = new AccessoryNonEmptyStringType(); + } + + if ($subjectArgumentType->isLowercaseString()->yes() && $replaceArgumentType->isLowercaseString()->yes()) { + $accessories[] = new AccessoryLowercaseStringType(); + } + + if ($subjectArgumentType->isUppercaseString()->yes() && $replaceArgumentType->isUppercaseString()->yes()) { + $accessories[] = new AccessoryUppercaseStringType(); } - if ($replaceArgumentType->isNonEmptyString()->yes()) { - return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); + + if (count($accessories) > 0) { + $accessories[] = new StringType(); + return new IntersectionType($accessories); } } } @@ -101,9 +123,30 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( if ($compareSuperTypes === $isStringSuperType) { return new StringType(); } elseif ($compareSuperTypes === $isArraySuperType) { - if (count($subjectArgumentType->getArrays()) > 0) { + $subjectArrays = $subjectArgumentType->getArrays(); + if (count($subjectArrays) > 0) { $result = []; - foreach ($subjectArgumentType->getArrays() as $arrayType) { + foreach ($subjectArrays as $arrayType) { + $constantArrays = $arrayType->getConstantArrays(); + + if ( + $constantArrays !== [] + && in_array($functionReflection->getName(), ['preg_replace', 'preg_replace_callback', 'preg_replace_callback_array'], true) + ) { + foreach ($constantArrays as $constantArray) { + $generalizedArray = $constantArray->generalizeValues(); + + $builder = ConstantArrayTypeBuilder::createEmpty(); + // turn all keys optional + foreach ($constantArray->getKeyTypes() as $keyType) { + $builder->setOffsetValueType($keyType, $generalizedArray->getOffsetValueType($keyType), true); + } + $result[] = $builder->getArray(); + } + + continue; + } + $result[] = $arrayType->generalizeValues(); } @@ -134,6 +177,20 @@ private function canReturnNull( Scope $scope, ): bool { + if ( + in_array($functionReflection->getName(), ['preg_replace', 'preg_replace_callback', 'preg_replace_callback_array'], true) + && count($functionCall->getArgs()) > 0 + ) { + $subjectArgumentType = $this->getSubjectType($functionReflection, $functionCall, $scope); + + if ( + $subjectArgumentType !== null + && $subjectArgumentType->isArray()->yes() + ) { + return false; + } + } + $possibleTypes = ParametersAcceptorSelector::selectFromArgs( $scope, $functionCall->getArgs(), diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index e3e3441b3e..88f95bf308 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.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\Accessory\AccessoryNumericStringType; @@ -22,7 +23,8 @@ use function count; use function in_array; -class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private PhpVersion $phpVersion) diff --git a/src/Type/Php/SetTypeFunctionTypeSpecifyingExtension.php b/src/Type/Php/SetTypeFunctionTypeSpecifyingExtension.php index 70526414bb..32373fcf7a 100644 --- a/src/Type/Php/SetTypeFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/SetTypeFunctionTypeSpecifyingExtension.php @@ -8,6 +8,7 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\ErrorType; use PHPStan\Type\FunctionTypeSpecifyingExtension; @@ -18,7 +19,8 @@ use function count; use function strtolower; -class SetTypeFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class SetTypeFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -78,9 +80,8 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $value, TypeCombinator::union(...$types), TypeSpecifierContext::createTruthy(), - true, $scope, - ); + )->setAlwaysOverwriteTypes(); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/SimpleXMLElementAsXMLMethodReturnTypeExtension.php b/src/Type/Php/SimpleXMLElementAsXMLMethodReturnTypeExtension.php index e6c390cd22..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; @@ -14,7 +15,8 @@ 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 dadfdaa478..1bf6d9854b 100644 --- a/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php +++ b/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Php; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Php\SimpleXMLElementProperty; use PHPStan\Reflection\PropertiesClassReflectionExtension; @@ -10,17 +11,18 @@ 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 BenevolentUnionType([new ObjectType($classReflection->getName()), new NullType()])); + return new SimpleXMLElementProperty($propertyName, $classReflection, new BenevolentUnionType([new ObjectType($classReflection->getName()), new NullType()])); } } diff --git a/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php b/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php index ea5da83872..78faf2d1d3 100644 --- a/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php @@ -4,6 +4,7 @@ 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; @@ -14,7 +15,8 @@ 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 diff --git a/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php b/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php index 5ce6d57598..45715f21be 100644 --- a/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php @@ -4,17 +4,19 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; -use PHPStan\Type\ArrayType; +use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicMethodReturnTypeExtension; -use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use SimpleXMLElement; use function extension_loaded; -class SimpleXMLElementXpathMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension +#[AutowiredService] +final class SimpleXMLElementXpathMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string @@ -29,11 +31,12 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { - if (!isset($methodCall->getArgs()[0])) { + $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(''); @@ -51,7 +54,9 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method 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 8829b4a4b8..b819df44c2 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -5,12 +5,15 @@ 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\InitializerExprTypeResolver; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -23,16 +26,19 @@ 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 @@ -59,7 +65,14 @@ public function getTypeFromFunctionCall( $formatType = $scope->getType($args[0]->value); $formatStrings = $formatType->getConstantStrings(); - $singlePlaceholderEarlyReturn = null; + $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) { @@ -83,87 +96,157 @@ public function getTypeFromFunctionCall( $allPatternsNonFalsy = false; } - // The printf format is %[argnum$][flags][width][.precision]specifier. - if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $constantString->getValue(), $matches) === 1) { - if ($matches[1] !== '') { + 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[1] === '0$') { + if ($matches['argnum'] === '0$') { return null; } - $checkArg = intval(substr($matches[1], 0, -1)); + $checkArg = intval(substr($matches['argnum'], 0, -1)); } else { $checkArg = 1; } - // constant string specifies a numbered argument that does not exist - if (!array_key_exists($checkArg, $args)) { + $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 - $checkArgType = $scope->getType($args[$checkArg]->value); - if ($matches[2] === 's' + if ( + $matches['specifier'] === 's' && ($checkArgType->isString()->yes() || $checkArgType->isInteger()->yes()) ) { - $singlePlaceholderEarlyReturn = $checkArgType->toString(); - } elseif ($matches[2] !== 's') { - $singlePlaceholderEarlyReturn = new IntersectionType([ - new StringType(), + 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; - break; } - if ($singlePlaceholderEarlyReturn !== null) { - return $singlePlaceholderEarlyReturn; + if (is_array($singlePlaceholderEarlyReturn) && count($singlePlaceholderEarlyReturn) > 0) { + return TypeCombinator::union(...$singlePlaceholderEarlyReturn); } if ($allPatternsNonFalsy) { - return new IntersectionType([ - new StringType(), - new AccessoryNonFalsyStringType(), - ]); + return $this->getStringReturnType(new AccessoryNonFalsyStringType(), $isLowercase); } $isNonEmpty = $allPatternsNonEmpty; - if ( - !$isNonEmpty - && $functionReflection->getName() === 'sprintf' - && count($args) >= 2 - && $formatType->isNonEmptyString()->yes() - ) { - $allArgsNonEmpty = true; + 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 (!$scope->getType($arg->value)->toString()->isNonEmptyString()->yes()) { - $allArgsNonEmpty = false; - break; + if (!$cb($scope->getType($arg->value))) { + return false; } } - if ($allArgsNonEmpty) { - $isNonEmpty = true; + 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 ($isNonEmpty) { - return new IntersectionType([ - new StringType(), - new AccessoryNonEmptyStringType(), - ]); + 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 new StringType(); + return null; } /** @@ -270,4 +353,23 @@ private function getConstantType(array $args, FunctionReflection $functionReflec 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 index 5939bf7ced..de22ba0a46 100644 --- a/src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; @@ -21,7 +22,8 @@ use function in_array; use function preg_match_all; -class SscanfFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class SscanfFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/StatDynamicReturnTypeExtension.php b/src/Type/Php/StatDynamicReturnTypeExtension.php index 7b8c08f194..b4dcd8bb7e 100644 --- a/src/Type/Php/StatDynamicReturnTypeExtension.php +++ b/src/Type/Php/StatDynamicReturnTypeExtension.php @@ -5,6 +5,7 @@ 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; @@ -18,7 +19,8 @@ use SplFileObject; use function in_array; -class StatDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension, DynamicMethodReturnTypeExtension +#[AutowiredService] +final class StatDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension, DynamicMethodReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php index ebb693672d..c965f8cd12 100644 --- a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php +++ b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php @@ -4,10 +4,13 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; +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\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; @@ -15,13 +18,17 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function array_diff; use function array_map; use function count; use function in_array; use function is_callable; use function mb_check_encoding; +use const MB_CASE_LOWER; +use const MB_CASE_UPPER; -class StrCaseFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class StrCaseFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { /** @@ -65,9 +72,36 @@ public function getTypeFromFunctionCall( } $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); @@ -75,6 +109,14 @@ public function getTypeFromFunctionCall( } 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()); @@ -101,25 +143,27 @@ public function getTypeFromFunctionCall( } } - if ($argType->isNumericString()->yes()) { - return new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]); + $accessoryTypes = []; + $argStringType = $argType->toString(); + if ($forceLowercase || ($keepLowercase && $argStringType->isLowercaseString()->yes())) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } + if ($forceUppercase || ($keepUppercase && $argStringType->isUppercaseString()->yes())) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); } - if ($argType->isNonFalsyString()->yes()) { - return new IntersectionType([ - new StringType(), - new AccessoryNonFalsyStringType(), - ]); + if ($argStringType->isNumericString()->yes()) { + $accessoryTypes[] = new AccessoryNumericStringType(); + } elseif ($argStringType->isNonFalsyString()->yes()) { + $accessoryTypes[] = new AccessoryNonFalsyStringType(); + } elseif ($argStringType->isNonEmptyString()->yes()) { + $accessoryTypes[] = new AccessoryNonEmptyStringType(); } - if ($argType->isNonEmptyString()->yes()) { - return new IntersectionType([ - new StringType(), - 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 index 8cf678ae56..2e24c9dcbd 100644 --- a/src/Type/Php/StrContainingTypeSpecifyingExtension.php +++ b/src/Type/Php/StrContainingTypeSpecifyingExtension.php @@ -13,6 +13,7 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -25,6 +26,7 @@ use function count; use function strtolower; +#[AutowiredService] final class StrContainingTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { @@ -66,7 +68,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n [$hackstackArg, $needleArg] = self::STR_CONTAINING_FUNCTIONS[strtolower($functionReflection->getName())]; $haystackType = $scope->getType($args[$hackstackArg]->value); - $needleType = $scope->getType($args[$needleArg]->value); + $needleType = $scope->getType($args[$needleArg]->value)->toString(); if ($needleType->isNonEmptyString()->yes() && $haystackType->isString()->yes()) { $accessories = [ @@ -90,18 +92,16 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $args[$hackstackArg]->value, new IntersectionType($accessories), $context, - false, $scope, - new BooleanAnd( - new NotIdentical( - $args[$needleArg]->value, - new String_(''), - ), - new FuncCall(new Name('FAUX_FUNCTION'), [ - new Arg($args[$needleArg]->value), - ]), + )->setRootExpr(new BooleanAnd( + new NotIdentical( + $args[$needleArg]->value, + new String_(''), ), - ); + new FuncCall(new Name('FAUX_FUNCTION'), [ + new Arg($args[$needleArg]->value), + ]), + )); } } diff --git a/src/Type/Php/StrIncrementDecrementFunctionReturnTypeExtension.php b/src/Type/Php/StrIncrementDecrementFunctionReturnTypeExtension.php index b739263e64..5c936e5f72 100644 --- a/src/Type/Php/StrIncrementDecrementFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrIncrementDecrementFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -23,7 +24,8 @@ use function str_split; use function stripos; -class StrIncrementDecrementFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class StrIncrementDecrementFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/StrPadFunctionReturnTypeExtension.php b/src/Type/Php/StrPadFunctionReturnTypeExtension.php index 3b11f5130e..a6e9f927c9 100644 --- a/src/Type/Php/StrPadFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrPadFunctionReturnTypeExtension.php @@ -4,10 +4,13 @@ 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; @@ -15,7 +18,8 @@ use PHPStan\Type\Type; use function count; -class StrPadFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class StrPadFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -44,15 +48,20 @@ public function getTypeFromFunctionCall( $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 3ba27a7f9e..585c02bf9b 100644 --- a/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php @@ -5,11 +5,14 @@ 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; @@ -22,7 +25,8 @@ use function str_repeat; use function strlen; -class StrRepeatFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class StrRepeatFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -93,6 +97,14 @@ public function getTypeFromFunctionCall( } } + 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); diff --git a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php index 34d58152dc..0c4a31496c 100644 --- a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php @@ -4,11 +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\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; @@ -16,7 +18,9 @@ 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; @@ -29,6 +33,7 @@ use function mb_str_split; use function str_split; +#[AutowiredService] final class StrSplitFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -51,14 +56,15 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, 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; @@ -67,47 +73,70 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $strings = $scope->getType($functionCall->getArgs()[2]->value)->getConstantStrings(); $values = array_unique(array_map(static fn (ConstantStringType $encoding): string => $encoding->getValue(), $strings)); - if (count($values) !== 1) { - return null; - } - - $encoding = $values[0]; - if (!$this->isSupportedEncoding($encoding)) { - return new ConstantBooleanType(false); + 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 null; - } - $stringType = $scope->getType($functionCall->getArgs()[0]->value); - - $constantStrings = $stringType->getConstantStrings(); - if (count($constantStrings) > 0) { - $results = []; - foreach ($constantStrings as $constantString) { - $items = $encoding === null - ? str_split($constantString->getValue(), $splitLength) - : @mb_str_split($constantString->getValue(), $splitLength, $encoding); - if ($items === false) { - throw new ShouldNotHappenException(); + 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); } - $results[] = self::createConstantArrayFrom($items, $scope); + return TypeCombinator::union(...$results); } + } - return TypeCombinator::union(...$results); + $isInputNonEmptyString = $stringType->isNonEmptyString()->yes(); + + if ($isInputNonEmptyString || $this->phpVersion->strSplitReturnsEmptyArray()) { + $returnValueType = TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()); + } else { + $returnValueType = new StringType(); } - $returnType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new StringType())); + $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)); + } - return $encoding === null && !$this->phpVersion->strSplitReturnsEmptyArray() - ? TypeCombinator::intersect($returnType, new NonEmptyArrayType()) - : $returnType; + return $returnType; } /** @@ -133,7 +162,7 @@ private static function createConstantArrayFrom(array $constantArray, Scope $sco $i++; } - return new ConstantArrayType($keyTypes, $valueTypes, $isList ? [$i] : [0], [], TrinaryLogic::createFromBoolean(array_is_list($constantArray))); + 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 1af80eafe6..01af622932 100644 --- a/src/Type/Php/StrTokFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrTokFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -14,7 +15,8 @@ use PHPStan\Type\Type; use function count; -class StrTokFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class StrTokFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php b/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php index 96a532ceef..a53b2c5c7d 100644 --- a/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -15,8 +16,10 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use function count; +use function in_array; -class StrWordCountFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class StrWordCountFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -33,14 +36,14 @@ public function getTypeFromFunctionCall( $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 e4a51cb833..c758d49095 100644 --- a/src/Type/Php/StrlenFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrlenFunctionReturnTypeExtension.php @@ -4,11 +4,9 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; -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\FloatType; use PHPStan\Type\IntegerRangeType; @@ -24,7 +22,8 @@ use function sort; use function strlen; -class StrlenFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class StrlenFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -44,25 +43,12 @@ public function getTypeFromFunctionCall( } $argType = $scope->getType($args[0]->value); - - if ($argType->isSuperTypeOf(new BooleanType())->yes()) { - $constantScalars = TypeCombinator::remove($argType, new BooleanType())->getConstantScalarTypes(); - if (count($constantScalars) > 0) { - $constantScalars[] = new ConstantBooleanType(true); - $constantScalars[] = new ConstantBooleanType(false); - } - } else { - $constantScalars = $argType->getConstantScalarTypes(); - } + $constantScalars = $argType->getConstantScalarValues(); $lengths = []; foreach ($constantScalars as $constantScalar) { - $stringScalar = $constantScalar->toString(); - if (!($stringScalar instanceof ConstantStringType)) { - $lengths = []; - break; - } - $length = strlen($stringScalar->getValue()); + $stringScalar = (string) $constantScalar; + $length = strlen($stringScalar); $lengths[] = $length; } 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 ef5e1af787..284759e59e 100644 --- a/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ 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; @@ -21,7 +22,8 @@ use function min; use function strtotime; -class StrtotimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class StrtotimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -31,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; } diff --git a/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php b/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php index 3328313d9a..3fa0403bab 100644 --- a/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -15,7 +16,8 @@ use function count; use function in_array; -class StrvalFamilyFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class StrvalFamilyFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { private const FUNCTIONS = [ diff --git a/src/Type/Php/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php index ae4bf10d5a..002340465b 100644 --- a/src/Type/Php/SubstrDynamicReturnTypeExtension.php +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.php @@ -4,9 +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\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; @@ -20,11 +24,17 @@ 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 in_array($functionReflection->getName(), ['substr', 'mb_substr'], true); @@ -37,74 +47,121 @@ public function getTypeFromFunctionCall( ): ?Type { $args = $functionCall->getArgs(); - if (count($args) === 0) { + if (count($args) < 2) { return null; } - if (count($args) >= 2) { - $string = $scope->getType($args[0]->value); - $offset = $scope->getType($args[1]->value); + $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; + $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) === 3) { + $length = $scope->getType($args[2]->value); + $positiveLength = IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($length)->yes(); + $maybeOneLength = !(new ConstantIntegerType(1))->isSuperTypeOf($length)->no(); + } - $constantStrings = $string->getConstantStrings(); - if ( - count($constantStrings) > 0 - && $offset instanceof ConstantIntegerType - && ($length === null || $length instanceof ConstantIntegerType) - ) { - $results = []; - foreach ($constantStrings as $constantString) { - if ($length !== null) { - if ($functionReflection->getName() === 'mb_substr') { - $substr = mb_substr($constantString->getValue(), $offset->getValue(), $length->getValue()); - } else { - $substr = substr($constantString->getValue(), $offset->getValue(), $length->getValue()); - } + $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 { - if ($functionReflection->getName() === 'mb_substr') { - $substr = mb_substr($constantString->getValue(), $offset->getValue()); - } else { - $substr = substr($constantString->getValue(), $offset->getValue()); - } + $substr = substr($constantString->getValue(), $offset->getValue()); } + } - if (is_bool($substr)) { + if (is_bool($substr)) { + if ($this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { $results[] = new ConstantBooleanType($substr); } else { - $results[] = new ConstantStringType($substr); + // 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); } - if ($string->isNonEmptyString()->yes() && ($negativeOffset || $zeroOffset && $positiveLength)) { - if ($string->isNonFalsyString()->yes() && !$maybeOneLength) { - return new IntersectionType([ - new StringType(), - new AccessoryNonFalsyStringType(), - ]); + return TypeCombinator::union(...$results); + } - } - return new IntersectionType([ - new StringType(), - new AccessoryNonEmptyStringType(), - ]); + $accessoryTypes = []; + $isNotEmpty = false; + if ($string->isLowercaseString()->yes()) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } + if ($string->isUppercaseString()->yes()) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } + if ($string->isNonEmptyString()->yes() && ($negativeOffset || $zeroOffset && $positiveLength)) { + $isNotEmpty = true; + if ($string->isNonFalsyString()->yes() && !$maybeOneLength) { + $accessoryTypes[] = new AccessoryNonFalsyStringType(); + } else { + $accessoryTypes[] = new AccessoryNonEmptyStringType(); } } + if (count($accessoryTypes) > 0) { + $accessoryTypes[] = new StringType(); + + if (!$isNotEmpty && $this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { + return TypeCombinator::union( + new ConstantBooleanType(false), + new IntersectionType($accessoryTypes), + ); + } + + return new IntersectionType($accessoryTypes); + } return null; } + 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 substr($string, $offset, $length); + } + } diff --git a/src/Type/Php/ThrowableReturnTypeExtension.php b/src/Type/Php/ThrowableReturnTypeExtension.php index 9437b82485..af236b4f2d 100644 --- a/src/Type/Php/ThrowableReturnTypeExtension.php +++ b/src/Type/Php/ThrowableReturnTypeExtension.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\BenevolentUnionType; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -18,6 +19,7 @@ use function in_array; use function strtolower; +#[AutowiredService] final class ThrowableReturnTypeExtension implements DynamicMethodReturnTypeExtension { diff --git a/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php b/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php index 1df3f180e5..3f3e09242d 100644 --- a/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.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\Constant\ConstantBooleanType; @@ -18,7 +19,8 @@ use const E_USER_NOTICE; use const E_USER_WARNING; -class TriggerErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +#[AutowiredService] +final class TriggerErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private PhpVersion $phpVersion) 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 380cfb5b59..8ed3c6b605 100644 --- a/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php @@ -6,6 +6,8 @@ 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\ReflectionProvider; use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper; @@ -15,7 +17,8 @@ use function count; use function in_array; -class TypeSpecifyingFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension, TypeSpecifierAwareExtension +#[AutowiredService] +final class TypeSpecifyingFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -25,7 +28,13 @@ class TypeSpecifyingFunctionsDynamicReturnTypeExtension implements DynamicFuncti /** * @param string[] $universalObjectCratesClasses */ - public function __construct(private ReflectionProvider $reflectionProvider, private bool $treatPhpDocTypesAsCertain, private array $universalObjectCratesClasses, private bool $nullContextForVoidReturningFunctions) + public function __construct( + private ReflectionProvider $reflectionProvider, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + #[AutowiredParameter] + private array $universalObjectCratesClasses, + ) { } @@ -67,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, $this->nullContextForVoidReturningFunctions); - } - - return $this->helper; + return $this->helper ??= new ImpossibleCheckTypeHelper($this->reflectionProvider, $this->typeSpecifier, $this->universalObjectCratesClasses, $this->treatPhpDocTypesAsCertain); } } diff --git a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php index b8a77540cb..d177092349 100644 --- a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php @@ -2,22 +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\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 function array_filter; use function count; +use function in_array; +use function is_array; use function version_compare; -class VersionCompareFunctionDynamicReturnTypeExtension implements 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'; @@ -29,21 +69,24 @@ public function getTypeFromFunctionCall( Scope $scope, ): ?Type { - if (count($functionCall->getArgs()) < 2) { + $args = $functionCall->getArgs(); + if (count($args) < 2) { return null; } - $version1Strings = $scope->getType($functionCall->getArgs()[0]->value)->getConstantStrings(); - $version2Strings = $scope->getType($functionCall->getArgs()[1]->value)->getConstantStrings(); + $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 = $scope->getType($functionCall->getArgs()[2]->value)->getConstantStrings(); + 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), @@ -61,11 +104,21 @@ public function getTypeFromFunctionCall( } $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 { @@ -74,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 8fd995882c..aeecba0db7 100644 --- a/src/Type/RecursionGuard.php +++ b/src/Type/RecursionGuard.php @@ -2,7 +2,7 @@ namespace PHPStan\Type; -class RecursionGuard +final class RecursionGuard { /** @var true[] */ 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 index 62708a2de3..3cc16fa182 100644 --- a/src/Type/Regex/RegexCapturingGroup.php +++ b/src/Type/Regex/RegexCapturingGroup.php @@ -7,10 +7,6 @@ final class RegexCapturingGroup { - private bool $forceNonOptional = false; - - private ?Type $forceType = null; - public function __construct( private readonly int $id, private readonly ?string $name, @@ -18,6 +14,8 @@ public function __construct( private readonly bool $inOptionalQuantification, private readonly RegexCapturingGroup|RegexNonCapturingGroup|null $parent, private readonly Type $type, + private readonly bool $forceNonOptional = false, + private readonly ?Type $forceType = null, ) { } @@ -27,20 +25,46 @@ public function getId(): int return $this->id; } - public function forceNonOptional(): void + public function forceNonOptional(): self { - $this->forceNonOptional = true; + return new self( + $this->id, + $this->name, + $this->alternation, + $this->inOptionalQuantification, + $this->parent, + $this->type, + true, + $this->forceType, + ); } - public function forceType(Type $type): void + public function forceType(Type $type): self { - $this->forceType = $type; + return new self( + $this->id, + $this->name, + $this->alternation, + $this->inOptionalQuantification, + $this->parent, + $type, + $this->forceNonOptional, + $this->forceType, + ); } - public function clearOverrides(): void + public function withParent(RegexCapturingGroup|RegexNonCapturingGroup $parent): self { - $this->forceNonOptional = false; - $this->forceType = null; + return new self( + $this->id, + $this->name, + $this->alternation, + $this->inOptionalQuantification, + $parent, + $this->type, + $this->forceNonOptional, + $this->forceType, + ); } public function resetsGroupCounter(): bool @@ -82,6 +106,11 @@ public function isOptional(): bool || $this->parent !== null && $this->parent->isOptional(); } + public function inOptionalQuantification(): bool + { + return $this->inOptionalQuantification; + } + public function inOptionalAlternation(): bool { if (!$this->inAlternation()) { @@ -123,4 +152,9 @@ public function getType(): Type 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 index 2b94fda6e2..0dd32830cd 100644 --- a/src/Type/Regex/RegexExpressionHelper.php +++ b/src/Type/Regex/RegexExpressionHelper.php @@ -6,14 +6,17 @@ use PhpParser\Node\Expr\BinaryOp\Concat; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function array_key_exists; +use function ltrim; use function strrpos; use function substr; +#[AutowiredService] final class RegexExpressionHelper { @@ -87,6 +90,8 @@ public function getPatternModifiers(string $pattern): ?string public function removeDelimitersAndModifiers(string $pattern): string { + $pattern = ltrim($pattern); + $endDelimiterPos = $this->getEndDelimiterPos($pattern); if ($endDelimiterPos === false) { @@ -147,6 +152,8 @@ public function getPatternDelimiters(Concat $concat, Scope $scope): array private function getPatternDelimiter(string $regex): ?string { + $regex = ltrim($regex); + if ($regex === '') { return null; } 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 index 6938ff8000..3162a1c783 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -9,6 +9,7 @@ use Hoa\File\Read; use Nette\Utils\RegexpException; use Nette\Utils\Strings; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; @@ -20,9 +21,11 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_values; use function count; use function in_array; use function is_int; +use function preg_replace; use function rtrim; use function sscanf; use function str_contains; @@ -31,9 +34,14 @@ use function substr; use function trim; +#[AutowiredService] final class RegexGroupParser { + private const NOT_SUPPORTED_MODIFIERS = [ + 'J', // rare modifier too complicated to support + ]; + private static ?Parser $parser = null; public function __construct( @@ -43,15 +51,10 @@ public function __construct( { } - /** - * @return array{array, list}|null - */ - public function parseGroups(string $regex): ?array + public function parseGroups(string $regex): ?RegexAstWalkResult { - if (self::$parser === null) { - /** @throws void */ - self::$parser = Llk::load(new Read(__DIR__ . '/../../../resources/RegexGrammar.pp')); - } + /** @throws void */ + self::$parser ??= Llk::load(new Read(__DIR__ . '/../../../resources/RegexGrammar.pp')); try { Strings::match('', $regex); @@ -60,6 +63,18 @@ public function parseGroups(string $regex): ?array return null; } + $modifiers = $this->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); @@ -67,57 +82,128 @@ public function parseGroups(string $regex): ?array return null; } + $this->updateAlternationAstRemoveVerticalBarsAndAddEmptyToken($ast); + $this->updateCapturingAstAddEmptyToken($ast); + $captureOnlyNamed = false; - $modifiers = $this->regexExpressionHelper->getPatternModifiers($regex) ?? ''; if ($this->phpVersion->supportsPregCaptureOnlyNamedGroups()) { $captureOnlyNamed = str_contains($modifiers, 'n'); } - $capturingGroups = []; - $alternationId = -1; - $captureGroupId = 100; - $markVerbs = []; - $this->walkRegexAst( + $astWalkResult = $this->walkRegexAst( $ast, null, - $alternationId, 0, false, null, - $captureGroupId, - $capturingGroups, - $markVerbs, $captureOnlyNamed, false, $modifiers, + RegexAstWalkResult::createEmpty(), ); - return [$capturingGroups, $markVerbs]; + $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; } - /** - * @param array $capturingGroups - * @param list $markVerbs - */ private function walkRegexAst( TreeNode $ast, ?RegexAlternation $alternation, - int &$alternationId, int $combinationIndex, bool $inOptionalQuantification, RegexCapturingGroup|RegexNonCapturingGroup|null $parentGroup, - int &$captureGroupId, - array &$capturingGroups, - array &$markVerbs, bool $captureOnlyNamed, bool $repeatedMoreThanOnce, string $patternModifiers, - ): void + RegexAstWalkResult $astWalkResult, + ): RegexAstWalkResult { $group = null; if ($ast->getId() === '#capturing') { + $astWalkResult = $astWalkResult->nextCaptureGroupId(); + $group = new RegexCapturingGroup( - $captureGroupId++, + $astWalkResult->getCaptureGroupId(), null, $alternation, $inOptionalQuantification, @@ -130,9 +216,11 @@ private function walkRegexAst( ); $parentGroup = $group; } elseif ($ast->getId() === '#namedcapturing') { + $astWalkResult = $astWalkResult->nextCaptureGroupId(); + $name = $ast->getChild(0)->getValueValue(); $group = new RegexCapturingGroup( - $captureGroupId++, + $astWalkResult->getCaptureGroupId(), $name, $alternation, $inOptionalQuantification, @@ -176,20 +264,19 @@ private function walkRegexAst( } if ($ast->getId() === '#alternation') { - $alternationId++; - $alternation = new RegexAlternation($alternationId, count($ast->getChildren())); + $astWalkResult = $astWalkResult->nextAlternationId(); + $alternation = new RegexAlternation($astWalkResult->getAlternationId(), count($ast->getChildren())); } if ($ast->getId() === '#mark') { - $markVerbs[] = $ast->getChild(0)->getValueValue(); - return; + return $astWalkResult->markVerb($ast->getChild(0)->getValueValue()); } if ( $group instanceof RegexCapturingGroup && (!$captureOnlyNamed || $group->isNamed()) ) { - $capturingGroups[$group->getId()] = $group; + $astWalkResult = $astWalkResult->addCapturingGroup($group); if ($alternation !== null) { $alternation->pushGroup($combinationIndex, $group); @@ -197,19 +284,16 @@ private function walkRegexAst( } foreach ($ast->getChildren() as $child) { - $this->walkRegexAst( + $astWalkResult = $this->walkRegexAst( $child, $alternation, - $alternationId, $combinationIndex, $inOptionalQuantification, $parentGroup, - $captureGroupId, - $capturingGroups, - $markVerbs, $captureOnlyNamed, $repeatedMoreThanOnce, $patternModifiers, + $astWalkResult, ); if ($ast->getId() !== '#alternation') { @@ -218,6 +302,8 @@ private function walkRegexAst( $combinationIndex++; } + + return $astWalkResult; } private function allowConstantTypes( @@ -294,35 +380,35 @@ private function getQuantificationRange(TreeNode $node): array private function createGroupType(TreeNode $group, bool $maybeConstant, string $patternModifiers): Type { - $isNonEmpty = TrinaryLogic::createMaybe(); - $isNonFalsy = TrinaryLogic::createMaybe(); - $isNumeric = TrinaryLogic::createMaybe(); - $inOptionalQuantification = false; - $onlyLiterals = []; + $rootAlternation = $this->getRootAlternation($group); + if ($rootAlternation !== null) { + $types = []; + foreach ($rootAlternation->getChildren() as $alternative) { + $types[] = $this->createGroupType($alternative, $maybeConstant, $patternModifiers); + } - $this->walkGroupAst( + return TypeCombinator::union(...$types); + } + + $walkResult = $this->walkGroupAst( $group, false, - $isNonEmpty, - $isNonFalsy, - $isNumeric, - $inOptionalQuantification, - $onlyLiterals, false, $patternModifiers, + RegexGroupWalkResult::createEmpty(), ); - if ($maybeConstant && $onlyLiterals !== null && $onlyLiterals !== []) { + if ($maybeConstant && $walkResult->getOnlyLiterals() !== null && $walkResult->getOnlyLiterals() !== []) { $result = []; - foreach ($onlyLiterals as $literal) { + foreach ($walkResult->getOnlyLiterals() as $literal) { $result[] = new ConstantStringType($literal); } return TypeCombinator::union(...$result); } - if ($isNumeric->yes()) { - if ($isNonFalsy->yes()) { + if ($walkResult->isNumeric()->yes()) { + if ($walkResult->isNonFalsy()->yes()) { return new IntersectionType([ new StringType(), new AccessoryNumericStringType(), @@ -331,39 +417,55 @@ private function createGroupType(TreeNode $group, bool $maybeConstant, string $p } $result = new IntersectionType([new StringType(), new AccessoryNumericStringType()]); - if (!$isNonEmpty->yes()) { + if (!$walkResult->isNonEmpty()->yes()) { return TypeCombinator::union(new ConstantStringType(''), $result); } return $result; - } elseif ($isNonFalsy->yes()) { + } elseif ($walkResult->isNonFalsy()->yes()) { return new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]); - } elseif ($isNonEmpty->yes()) { + } elseif ($walkResult->isNonEmpty()->yes()) { return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); } return new StringType(); } - /** - * @param array|null $onlyLiterals - */ + 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, - TrinaryLogic &$isNonEmpty, - TrinaryLogic &$isNonFalsy, - TrinaryLogic &$isNumeric, - bool &$inOptionalQuantification, - ?array &$onlyLiterals, bool $inClass, string $patternModifiers, - ): void + RegexGroupWalkResult $walkResult, + ): RegexGroupWalkResult { $children = $ast->getChildren(); if ( $ast->getId() === '#concatenation' && count($children) > 0 + && !$walkResult->isInOptionalQuantification() ) { $meaningfulTokens = 0; foreach ($children as $child) { @@ -379,87 +481,113 @@ private function walkGroupAst( } // a single token non-falsy on its own - $isNonFalsy = TrinaryLogic::createYes(); + $walkResult = $walkResult->nonFalsy(TrinaryLogic::createYes()); break; } if ($meaningfulTokens > 0) { - $isNonEmpty = TrinaryLogic::createYes(); + $walkResult = $walkResult->nonEmpty(TrinaryLogic::createYes()); // two non-empty tokens concatenated results in a non-falsy string if ($meaningfulTokens > 1 && !$inAlternation) { - $isNonFalsy = TrinaryLogic::createYes(); + $walkResult = $walkResult->nonFalsy(TrinaryLogic::createYes()); } } } elseif ($ast->getId() === '#quantification') { [$min] = $this->getQuantificationRange($ast); if ($min === 0) { - $inOptionalQuantification = true; + $walkResult = $walkResult->inOptionalQuantification(true); } - if ($min >= 1) { - $isNonEmpty = TrinaryLogic::createYes(); - $inOptionalQuantification = false; - } - if ($min >= 2 && !$inAlternation) { - $isNonFalsy = TrinaryLogic::createYes(); + + if (!$walkResult->isInOptionalQuantification()) { + if ($min >= 1) { + $walkResult = $walkResult->nonEmpty(TrinaryLogic::createYes()); + } + if ($min >= 2 && !$inAlternation) { + $walkResult = $walkResult->nonFalsy(TrinaryLogic::createYes()); + } } - $onlyLiterals = null; - } elseif ($ast->getId() === '#class' && $onlyLiterals !== null) { + $walkResult = $walkResult->onlyLiterals(null); + } elseif ($ast->getId() === '#class' && $walkResult->getOnlyLiterals() !== null) { $inClass = true; $newLiterals = []; foreach ($children as $child) { - $oldLiterals = $onlyLiterals; + $oldLiterals = $walkResult->getOnlyLiterals(); - $this->getLiteralValue($child, $oldLiterals, true, $patternModifiers); + $this->getLiteralValue($child, $oldLiterals, true, $patternModifiers, true); foreach ($oldLiterals ?? [] as $oldLiteral) { $newLiterals[] = $oldLiteral; } } - $onlyLiterals = $newLiterals; + $walkResult = $walkResult->onlyLiterals($newLiterals); } elseif ($ast->getId() === 'token') { - $literalValue = $this->getLiteralValue($ast, $onlyLiterals, !$inClass, $patternModifiers); + $onlyLiterals = $walkResult->getOnlyLiterals(); + $literalValue = $this->getLiteralValue($ast, $onlyLiterals, !$inClass, $patternModifiers, false); + $walkResult = $walkResult->onlyLiterals($onlyLiterals); + if ($literalValue !== null) { if (Strings::match($literalValue, '/^\d+$/') === null) { - $isNumeric = TrinaryLogic::createNo(); - } elseif ($isNumeric->maybe()) { - $isNumeric = TrinaryLogic::createYes(); + $walkResult = $walkResult->numeric(TrinaryLogic::createNo()); + } elseif ($walkResult->isNumeric()->maybe()) { + $walkResult = $walkResult->numeric(TrinaryLogic::createYes()); } - if (!$inOptionalQuantification) { - $isNonEmpty = TrinaryLogic::createYes(); + if (!$walkResult->isInOptionalQuantification() && $literalValue !== '') { + $walkResult = $walkResult->nonEmpty(TrinaryLogic::createYes()); } } - } elseif (!in_array($ast->getId(), ['#capturing', '#namedcapturing'], true)) { - $onlyLiterals = null; + } elseif (!in_array($ast->getId(), ['#capturing', '#namedcapturing', '#alternation'], true)) { + $walkResult = $walkResult->onlyLiterals(null); } if ($ast->getId() === '#alternation') { - $inAlternation = true; + $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') { - $isNumeric = TrinaryLogic::createNo(); + $walkResult = $walkResult->numeric(TrinaryLogic::createNo()); } foreach ($children as $child) { - $this->walkGroupAst( + $walkResult = $this->walkGroupAst( $child, $inAlternation, - $isNonEmpty, - $isNonFalsy, - $isNumeric, - $inOptionalQuantification, - $onlyLiterals, $inClass, $patternModifiers, + $walkResult, ); } + + return $walkResult; } private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool &$isNonFalsy): bool @@ -476,12 +604,12 @@ private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool } } - $literal = $this->getLiteralValue($node, $onlyLiterals, false, $patternModifiers); + $literal = $this->getLiteralValue($node, $onlyLiterals, false, $patternModifiers, false); if ($literal !== null) { if ($literal !== '' && $literal !== '0') { $isNonFalsy = true; } - return false; + return $literal === ''; } foreach ($node->getChildren() as $child) { @@ -496,7 +624,7 @@ private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool /** * @param array|null $onlyLiterals */ - private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $appendLiterals, string $patternModifiers): ?string + private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $appendLiterals, string $patternModifiers, bool $inCharacterClass): ?string { if ($node->getId() !== 'token') { return null; @@ -508,32 +636,39 @@ private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $ap if ( in_array($token, [ - 'literal', 'escaped_end_class', + '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_literal', + '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 - && in_array($token, ['literal', 'range', 'class_', '_class_literal'], true) && $onlyLiterals !== null - && !in_array($value, ['.'], true) ) { - if ($onlyLiterals === []) { - $onlyLiterals = [$value]; + if ( + in_array($value, ['.'], true) + && !($isEscaped || $inCharacterClass) + ) { + $onlyLiterals = null; } else { - foreach ($onlyLiterals as &$literal) { - $literal .= $value; + if ($onlyLiterals === []) { + $onlyLiterals = [$value]; + } else { + foreach ($onlyLiterals as &$literal) { + $literal .= $value; + } } } } @@ -576,7 +711,7 @@ private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $ap } } - if ($token === 'anchor' || $token === 'match_point_reset') { + if (in_array($token, ['anchor', 'match_point_reset'], true)) { return ''; } 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/ResourceType.php b/src/Type/ResourceType.php index 4327f30bde..3e83e3d819 100644 --- a/src/Type/ResourceType.php +++ b/src/Type/ResourceType.php @@ -81,8 +81,7 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], - [], - TrinaryLogic::createYes(), + isList: TrinaryLogic::createYes(), ); } @@ -91,6 +90,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + public function isOffsetAccessLegal(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -121,12 +125,4 @@ public function toPhpDocNode(): TypeNode return new IdentifierTypeNode('resource'); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self(); - } - } diff --git a/src/Type/SimultaneousTypeTraverser.php b/src/Type/SimultaneousTypeTraverser.php index f0e0e3dfe7..d9a3d69783 100644 --- a/src/Type/SimultaneousTypeTraverser.php +++ b/src/Type/SimultaneousTypeTraverser.php @@ -2,7 +2,10 @@ namespace PHPStan\Type; -class SimultaneousTypeTraverser +/** + * @api + */ +final class SimultaneousTypeTraverser { /** @var callable(Type $left, Type $right, callable(Type, Type): Type $traverse): Type */ diff --git a/src/Type/StaticMethodTypeSpecifyingExtension.php b/src/Type/StaticMethodTypeSpecifyingExtension.php index dbb6a49ffa..7421f3b896 100644 --- a/src/Type/StaticMethodTypeSpecifyingExtension.php +++ b/src/Type/StaticMethodTypeSpecifyingExtension.php @@ -28,6 +28,7 @@ interface StaticMethodTypeSpecifyingExtension { + /** @return class-string */ public function getClass(): string; public function isStaticMethodSupported(MethodReflection $staticMethodReflection, StaticCall $node, TypeSpecifierContext $context): bool; diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index 766cede665..2d5a7c3ad8 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -5,12 +5,11 @@ 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\ClassReflection; -use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\PropertyReflection; -use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; @@ -89,8 +88,7 @@ public function getStaticObjectType(): ObjectType $this->classReflection->getName(), $this->classReflection->typeMapToList($typeMap), $this->subtractedType, - null, - $this->classReflection->varianceMapToList($varianceMap), + variances: $this->classReflection->varianceMapToList($varianceMap), ); } @@ -100,9 +98,6 @@ public function getStaticObjectType(): ObjectType return $this->staticObjectType; } - /** - * @return string[] - */ public function getReferencedClasses(): array { return $this->getStaticObjectType()->getReferencedClasses(); @@ -133,32 +128,27 @@ public function getConstantStrings(): array return $this->getStaticObjectType()->getConstantStrings(); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } if (!$type instanceof static) { return AcceptsResult::createNo(); } - return $this->getStaticObjectType()->acceptsWithReason($type->getStaticObjectType(), $strictTypes); + 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) { @@ -170,14 +160,14 @@ public function isSuperTypeOf(Type $type): TrinaryLogic } } - return $result->and(TrinaryLogic::createMaybe()); + 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 @@ -219,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(); } @@ -294,11 +284,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); @@ -315,7 +305,7 @@ public function hasConstant(string $constantName): TrinaryLogic return $this->getStaticObjectType()->hasConstant($constantName); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { return $this->getStaticObjectType()->getConstant($constantName); } @@ -405,6 +395,11 @@ 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(); @@ -415,6 +410,11 @@ 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); @@ -435,9 +435,14 @@ public function popArray(): Type return $this->getStaticObjectType()->popArray(); } - public function searchArray(Type $needleType): Type + public function reverseArray(TrinaryLogic $preserveKeys): Type { - return $this->getStaticObjectType()->searchArray($needleType); + return $this->getStaticObjectType()->reverseArray($preserveKeys); + } + + public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type + { + return $this->getStaticObjectType()->searchArray($needleType, $strict); } public function shiftArray(): Type @@ -450,6 +455,16 @@ 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(); @@ -555,9 +570,19 @@ public function isLiteralString(): TrinaryLogic return $this->getStaticObjectType()->isLiteralString(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic { - return $this->getStaticObjectType()->isClassStringType(); + return $this->getStaticObjectType()->isLowercaseString(); + } + + public function isUppercaseString(): TrinaryLogic + { + return $this->getStaticObjectType()->isUppercaseString(); + } + + public function isClassString(): TrinaryLogic + { + return $this->getStaticObjectType()->isClassString(); } public function getClassStringObjectType(): Type @@ -630,6 +655,11 @@ 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(); @@ -722,17 +752,4 @@ public function toPhpDocNode(): TypeNode return new IdentifierTypeNode('static'); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if ($reflectionProvider->hasClass($properties['baseClass'])) { - return new self($reflectionProvider->getClass($properties['baseClass']), $properties['subtractedType'] ?? null); - } - - return new ErrorType(); - } - } diff --git a/src/Type/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 cc114d5c34..0ff25dc124 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -5,10 +5,10 @@ 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\ExtendedMethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; @@ -51,22 +51,12 @@ public function getConstantStrings(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { return AcceptsResult::createYes(); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic - { - return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; - } - - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { if ($acceptingType instanceof self) { return AcceptsResult::createYes(); @@ -78,21 +68,21 @@ public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): return AcceptsResult::createMaybe(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof self) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($otherType instanceof MixedType && !$otherType instanceof TemplateMixedType) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } public function equals(Type $type): bool @@ -135,7 +125,7 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { throw new ShouldNotHappenException(); } @@ -175,7 +165,7 @@ public function hasConstant(string $constantName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { throw new ShouldNotHappenException(); } @@ -275,7 +265,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -395,6 +395,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { return TemplateTypeMap::createEmpty(); @@ -435,12 +440,4 @@ public function toPhpDocNode(): TypeNode return new IdentifierTypeNode('mixed'); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self(); - } - } diff --git a/src/Type/StringAlwaysAcceptingObjectWithToStringType.php b/src/Type/StringAlwaysAcceptingObjectWithToStringType.php index 0130d7e094..feb18e97d6 100644 --- a/src/Type/StringAlwaysAcceptingObjectWithToStringType.php +++ b/src/Type/StringAlwaysAcceptingObjectWithToStringType.php @@ -3,12 +3,11 @@ namespace PHPStan\Type; use PHPStan\Reflection\ReflectionProviderStaticAccessor; -use PHPStan\TrinaryLogic; class StringAlwaysAcceptingObjectWithToStringType extends StringType { - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); @@ -19,25 +18,25 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return parent::isSuperTypeOf($type); } - $result = TrinaryLogic::createNo(); + $result = IsSuperTypeOfResult::createNo(); $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); foreach ($thatClassNames as $thatClassName) { if (!$reflectionProvider->hasClass($thatClassName)) { - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } $typeClass = $reflectionProvider->getClass($thatClassName); - $result = $result->or(TrinaryLogic::createFromBoolean($typeClass->hasNativeMethod('__toString'))); + $result = $result->or(IsSuperTypeOfResult::createFromBoolean($typeClass->hasNativeMethod('__toString'))); } return $result; } - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { $thatClassNames = $type->getObjectClassNames(); if ($thatClassNames === []) { - return parent::acceptsWithReason($type, $strictTypes); + return parent::accepts($type, $strictTypes); } $result = AcceptsResult::createNo(); diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 5fe9ae444a..44fa96ab15 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -10,6 +10,7 @@ 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; @@ -109,19 +110,14 @@ public function unsetOffset(Type $offsetType): Type return new ErrorType(); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof self) { return AcceptsResult::createYes(); } if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } $thatClassNames = $type->getObjectClassNames(); @@ -175,8 +171,7 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], - [], - TrinaryLogic::createYes(), + isList: TrinaryLogic::createYes(), ); } @@ -185,6 +180,18 @@ 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(); @@ -240,7 +247,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } @@ -262,12 +279,16 @@ public function isScalar(): TrinaryLogic 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->isClassStringType()->yes()) { + if ($this->isClassString()->yes()) { return TrinaryLogic::createMaybe(); } return TrinaryLogic::createNo(); @@ -301,12 +322,4 @@ public function toPhpDocNode(): TypeNode return new IdentifierTypeNode('string'); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self(); - } - } 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 963f98c191..39d4949ca8 100644 --- a/src/Type/ThisType.php +++ b/src/Type/ThisType.php @@ -5,8 +5,6 @@ use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\ReflectionProviderStaticAccessor; -use PHPStan\TrinaryLogic; use function sprintf; /** @api */ @@ -34,7 +32,7 @@ public function describe(VerbosityLevel $level): string return sprintf('$this(%s)', $this->getStaticObjectType()->describe($level)); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { return $this->getStaticObjectType()->isSuperTypeOf($type); @@ -46,7 +44,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic $parent = new parent($this->getClassReflection(), $this->getSubtractedType()); - return $parent->isSuperTypeOf($type)->and(TrinaryLogic::createMaybe()); + return $parent->isSuperTypeOf($type)->and(IsSuperTypeOfResult::createMaybe()); } public function changeSubtractedType(?Type $subtractedType): Type @@ -87,17 +85,4 @@ public function toPhpDocNode(): TypeNode return new ThisTypeNode(); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if ($reflectionProvider->hasClass($properties['baseClass'])) { - return new self($reflectionProvider->getClass($properties['baseClass']), $properties['subtractedType'] ?? null); - } - - return new ErrorType(); - } - } diff --git a/src/Type/Traits/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 2b3b4a45c5..c6efe96950 100644 --- a/src/Type/Traits/ConstantNumericComparisonTypeTrait.php +++ b/src/Type/Traits/ConstantNumericComparisonTypeTrait.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Traits; +use PHPStan\Php\PhpVersion; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\IntegerRangeType; @@ -13,7 +14,7 @@ trait ConstantNumericComparisonTypeTrait { - public function getSmallerType(): Type + public function getSmallerType(PhpVersion $phpVersion): Type { $subtractedTypes = [ new ConstantBooleanType(true), @@ -29,7 +30,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($this->value), @@ -43,7 +44,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(), @@ -59,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), diff --git a/src/Type/Traits/ConstantScalarTypeTrait.php b/src/Type/Traits/ConstantScalarTypeTrait.php index 5a8d6dcfc5..f458512622 100644 --- a/src/Type/Traits/ConstantScalarTypeTrait.php +++ b/src/Type/Traits/ConstantScalarTypeTrait.php @@ -10,45 +10,41 @@ use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\LooseComparisonHelper; use PHPStan\Type\Type; trait ConstantScalarTypeTrait { - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof self) { return AcceptsResult::createFromBoolean($this->equals($type)); } if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } - return parent::acceptsWithReason($type, $strictTypes)->and(AcceptsResult::createMaybe()); + 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 TrinaryLogic::createFromBoolean($this->equals($type)); + 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 @@ -62,10 +58,14 @@ public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType } if ($type->isConstantArray()->yes() && $type->isIterableAtLeastOnce()->no()) { - // @phpstan-ignore equal.notAllowed, equal.invalid + // @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); } @@ -74,27 +74,27 @@ 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(); diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index a749a6fae0..0e1dee9904 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -3,10 +3,10 @@ namespace PHPStan\Type\Traits; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; -use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; @@ -14,6 +14,7 @@ use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\LateResolvableType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; @@ -48,25 +49,20 @@ public function getConstantStrings(): array return $this->resolve()->getConstantStrings(); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { return $this->resolve()->accepts($type, $strictTypes); } - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult - { - return $this->resolve()->acceptsWithReason($type, $strictTypes); - } - - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { return $this->isSuperTypeOfDefault($type); } - private function isSuperTypeOfDefault(Type $type): TrinaryLogic + private function isSuperTypeOfDefault(Type $type): IsSuperTypeOfResult { if ($type instanceof NeverType) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type instanceof LateResolvableType) { @@ -76,7 +72,7 @@ private function isSuperTypeOfDefault(Type $type): TrinaryLogic $isSuperType = $this->resolve()->isSuperTypeOf($type); if (!$this->isResolvable()) { - $isSuperType = $isSuperType->and(TrinaryLogic::createMaybe()); + $isSuperType = $isSuperType->and(IsSuperTypeOfResult::createMaybe()); } return $isSuperType; @@ -107,7 +103,7 @@ public function hasProperty(string $propertyName): TrinaryLogic return $this->resolve()->hasProperty($propertyName); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->resolve()->getProperty($propertyName, $scope); } @@ -147,7 +143,7 @@ public function hasConstant(string $constantName): TrinaryLogic return $this->resolve()->hasConstant($constantName); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { return $this->resolve()->getConstant($constantName); } @@ -252,6 +248,11 @@ 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(); @@ -262,6 +263,11 @@ 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); @@ -282,9 +288,14 @@ public function popArray(): Type return $this->resolve()->popArray(); } - public function searchArray(Type $needleType): Type + 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); + return $this->resolve()->searchArray($needleType, $strict); } public function shiftArray(): Type @@ -297,6 +308,16 @@ 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(); @@ -357,14 +378,19 @@ public function toArrayKey(): Type return $this->resolve()->toArrayKey(); } - public function isSmallerThan(Type $otherType): TrinaryLogic + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this->resolve()->toCoercedArgumentType($strictTypes); + } + + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { - return $this->resolve()->isSmallerThan($otherType); + return $this->resolve()->isSmallerThan($otherType, $phpVersion); } - public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { - return $this->resolve()->isSmallerThanOrEqual($otherType); + return $this->resolve()->isSmallerThanOrEqual($otherType, $phpVersion); } public function isNull(): TrinaryLogic @@ -442,9 +468,19 @@ public function isLiteralString(): TrinaryLogic return $this->resolve()->isLiteralString(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return $this->resolve()->isLowercaseString(); + } + + public function isUppercaseString(): TrinaryLogic { - return $this->resolve()->isClassStringType(); + return $this->resolve()->isUppercaseString(); + } + + public function isClassString(): TrinaryLogic + { + return $this->resolve()->isClassString(); } public function getClassStringObjectType(): Type @@ -472,24 +508,24 @@ public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType return new BooleanType(); } - public function getSmallerType(): Type + public function getSmallerType(PhpVersion $phpVersion): Type { - return $this->resolve()->getSmallerType(); + return $this->resolve()->getSmallerType($phpVersion); } - public function getSmallerOrEqualType(): Type + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { - return $this->resolve()->getSmallerOrEqualType(); + return $this->resolve()->getSmallerOrEqualType($phpVersion); } - public function getGreaterType(): Type + public function getGreaterType(PhpVersion $phpVersion): Type { - return $this->resolve()->getGreaterType(); + return $this->resolve()->getGreaterType($phpVersion); } - public function getGreaterOrEqualType(): Type + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { - return $this->resolve()->getGreaterOrEqualType(); + return $this->resolve()->getGreaterOrEqualType($phpVersion); } public function inferTemplateTypes(Type $receivedType): TemplateTypeMap @@ -502,7 +538,7 @@ public function tryRemove(Type $typeToRemove): ?Type return $this->resolve()->tryRemove($typeToRemove); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { $result = $this->resolve(); @@ -513,42 +549,37 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic return $otherType->isSuperTypeOf($result); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic - { - return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; - } - - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { $result = $this->resolve(); if ($result instanceof CompoundType) { - return $result->isAcceptedWithReasonBy($acceptingType, $strictTypes); + return $result->isAcceptedBy($acceptingType, $strictTypes); } - return $acceptingType->acceptsWithReason($result, $strictTypes); + return $acceptingType->accepts($result, $strictTypes); } - public function isGreaterThan(Type $otherType): TrinaryLogic + public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { $result = $this->resolve(); if ($result instanceof CompoundType) { - return $result->isGreaterThan($otherType); + return $result->isGreaterThan($otherType, $phpVersion); } - return $otherType->isSmallerThan($result); + return $otherType->isSmallerThan($result, $phpVersion); } - public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic + public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { $result = $this->resolve(); if ($result instanceof CompoundType) { - return $result->isGreaterThanOrEqual($otherType); + return $result->isGreaterThanOrEqual($otherType, $phpVersion); } - return $otherType->isSmallerThanOrEqual($result); + return $otherType->isSmallerThanOrEqual($result, $phpVersion); } public function exponentiate(Type $exponent): Type @@ -563,11 +594,7 @@ public function getFiniteTypes(): array public function resolve(): Type { - if ($this->result === null) { - return $this->result = $this->getResult(); - } - - return $this->result; + return $this->result ??= $this->getResult(); } abstract protected function getResult(): Type; diff --git a/src/Type/Traits/MaybeArrayTypeTrait.php b/src/Type/Traits/MaybeArrayTypeTrait.php index da064c82b7..a4080f0aa1 100644 --- a/src/Type/Traits/MaybeArrayTypeTrait.php +++ b/src/Type/Traits/MaybeArrayTypeTrait.php @@ -39,6 +39,11 @@ public function isList(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + public function getKeysArray(): Type { return new ErrorType(); @@ -49,6 +54,11 @@ 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(); @@ -69,7 +79,12 @@ public function popArray(): Type return new ErrorType(); } - public function searchArray(Type $needleType): Type + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return new ErrorType(); + } + + public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type { return new ErrorType(); } @@ -84,4 +99,14 @@ 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/MaybeObjectTypeTrait.php b/src/Type/Traits/MaybeObjectTypeTrait.php index b8097947b4..4625da358b 100644 --- a/src/Type/Traits/MaybeObjectTypeTrait.php +++ b/src/Type/Traits/MaybeObjectTypeTrait.php @@ -2,13 +2,13 @@ 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\ExtendedMethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; @@ -45,14 +45,14 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - $property = new DummyPropertyReflection(); + $property = new DummyPropertyReflection($propertyName); return new CallbackUnresolvedPropertyPrototypeReflection( $property, $property->getDeclaringClass(), @@ -97,9 +97,9 @@ public function hasConstant(string $constantName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { - return new DummyConstantReflection($constantName); + return new DummyClassConstantReflection($constantName); } public function isCloneable(): TrinaryLogic diff --git a/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php b/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php index 6e83395e95..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(); diff --git a/src/Type/Traits/NonArrayTypeTrait.php b/src/Type/Traits/NonArrayTypeTrait.php index def99b0e94..5f586ad1a6 100644 --- a/src/Type/Traits/NonArrayTypeTrait.php +++ b/src/Type/Traits/NonArrayTypeTrait.php @@ -39,6 +39,11 @@ public function isList(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + public function getKeysArray(): Type { return new ErrorType(); @@ -49,6 +54,11 @@ 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(); @@ -69,7 +79,12 @@ public function popArray(): Type return new ErrorType(); } - public function searchArray(Type $needleType): Type + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return new ErrorType(); + } + + public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type { return new ErrorType(); } @@ -84,4 +99,14 @@ 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/NonObjectTypeTrait.php b/src/Type/Traits/NonObjectTypeTrait.php index 3cba4b5bca..d16b86c9b1 100644 --- a/src/Type/Traits/NonObjectTypeTrait.php +++ b/src/Type/Traits/NonObjectTypeTrait.php @@ -2,10 +2,10 @@ namespace PHPStan\Type\Traits; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; -use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; @@ -36,7 +36,7 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { throw new ShouldNotHappenException(); } @@ -76,7 +76,7 @@ public function hasConstant(string $constantName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { throw new ShouldNotHappenException(); } diff --git a/src/Type/Traits/ObjectTypeTrait.php b/src/Type/Traits/ObjectTypeTrait.php index 0c37e439c7..c600f2d74a 100644 --- a/src/Type/Traits/ObjectTypeTrait.php +++ b/src/Type/Traits/ObjectTypeTrait.php @@ -3,13 +3,13 @@ namespace PHPStan\Type\Traits; use PHPStan\Php\PhpVersion; +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\ExtendedMethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; @@ -21,6 +21,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; trait ObjectTypeTrait { @@ -56,14 +57,14 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - $property = new DummyPropertyReflection(); + $property = new DummyPropertyReflection($propertyName); return new CallbackUnresolvedPropertyPrototypeReflection( $property, $property->getDeclaringClass(), @@ -108,9 +109,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 getConstantStrings(): array @@ -198,7 +199,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -263,4 +274,13 @@ public function toArrayKey(): Type return new StringType(); } + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this, $this->toString()); + } + + return $this; + } + } diff --git a/src/Type/Traits/UndecidedComparisonCompoundTypeTrait.php b/src/Type/Traits/UndecidedComparisonCompoundTypeTrait.php index e40b72fad4..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 3ac0a41c24..b9c88f51e6 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -5,11 +5,11 @@ 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\ClassReflection; -use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; @@ -28,7 +28,7 @@ interface Type { /** - * @return string[] + * @return list */ public function getReferencedClasses(): array; @@ -55,7 +55,7 @@ public function isObject(): TrinaryLogic; public function isEnum(): TrinaryLogic; - /** @return list */ + /** @return list */ public function getArrays(): array; /** @return list */ @@ -64,18 +64,9 @@ public function getConstantArrays(): array; /** @return list */ public function getConstantStrings(): array; - public function accepts(Type $type, bool $strictTypes): TrinaryLogic; + public function accepts(Type $type, bool $strictTypes): AcceptsResult; - /** - * This is like accepts() but gives reasons - * why the type was not/might not be accepted in some non-intuitive scenarios. - * - * In PHPStan 2.0 this method will be removed and the return type of accepts() - * will change to AcceptsResult. - */ - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult; - - public function isSuperTypeOf(Type $type): TrinaryLogic; + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult; public function equals(Type $type): bool; @@ -85,7 +76,7 @@ public function canAccessProperties(): TrinaryLogic; public function hasProperty(string $propertyName): TrinaryLogic; - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection; + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection; public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection; @@ -101,7 +92,7 @@ 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; @@ -143,10 +134,14 @@ public function setExistingOffsetValueType(Type $offsetType, Type $valueType): T 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; @@ -155,12 +150,18 @@ public function intersectKeyArray(Type $otherArraysType): Type; public function popArray(): Type; - public function searchArray(Type $needleType): 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 */ @@ -207,9 +208,20 @@ public function toArray(): Type; public function toArrayKey(): Type; - public function isSmallerThan(Type $otherType): TrinaryLogic; + /** + * Tells how a type might change when passed to an argument + * or assigned to a typed property. + * + * Example: int is accepted by int|float with strict_types = 1 + * Stringable is accepted by string|Stringable even without strict_types. + * + * Note: Logic with $strictTypes=false is mostly not implemented in Type subclasses. + */ + public function toCoercedArgumentType(bool $strictTypes): self; - public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic; + 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. @@ -253,7 +265,11 @@ public function isNonFalsyString(): TrinaryLogic; public function isLiteralString(): TrinaryLogic; - public function isClassStringType(): TrinaryLogic; + public function isLowercaseString(): TrinaryLogic; + + public function isUppercaseString(): TrinaryLogic; + + public function isClassString(): TrinaryLogic; public function isVoid(): TrinaryLogic; @@ -261,13 +277,13 @@ public function isScalar(): TrinaryLogic; public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType; - public function getSmallerType(): Type; + public function getSmallerType(PhpVersion $phpVersion): Type; - public function getSmallerOrEqualType(): Type; + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type; - public function getGreaterType(): Type; + public function getGreaterType(PhpVersion $phpVersion): Type; - public function getGreaterOrEqualType(): Type; + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type; /** * Returns actual template type for a given object. @@ -309,7 +325,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap; * which the receiver type was * found. * - * @return TemplateTypeReference[] + * @return list */ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array; @@ -343,9 +359,4 @@ public function tryRemove(Type $typeToRemove): ?Type; public function generalize(GeneralizePrecision $precision): Type; - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self; - } diff --git a/src/Type/TypeAlias.php b/src/Type/TypeAlias.php index 10d6c09922..17bd6373e5 100644 --- a/src/Type/TypeAlias.php +++ b/src/Type/TypeAlias.php @@ -7,7 +7,7 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -class TypeAlias +final class TypeAlias { private ?Type $resolvedType = null; @@ -28,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/TypeCombinator.php b/src/Type/TypeCombinator.php index c0564cb4a4..d79af61977 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -2,9 +2,12 @@ namespace PHPStan\Type; +use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\HasPropertyType; @@ -35,9 +38,13 @@ use function md5; use function sprintf; use function usort; +use const PHP_INT_MAX; +use const PHP_INT_MIN; -/** @api */ -class TypeCombinator +/** + * @api + */ +final class TypeCombinator { public static function addNull(Type $type): Type @@ -181,6 +188,7 @@ public static function union(Type ...$types): Type $scalarTypes = []; $hasGenericScalarTypes = []; $enumCaseTypes = []; + $integerRangeTypes = []; for ($i = 0; $i < $typesCount; $i++) { if ($types[$i] instanceof ConstantScalarType) { $type = $types[$i]; @@ -208,6 +216,13 @@ public static function union(Type ...$types): Type continue; } + if ($types[$i] instanceof IntegerRangeType) { + $integerRangeTypes[] = $types[$i]; + unset($types[$i]); + + continue; + } + if (!$types[$i]->isArray()->yes()) { continue; } @@ -221,6 +236,12 @@ public static function union(Type ...$types): Type } $enumCaseTypes = array_values($enumCaseTypes); + usort( + $integerRangeTypes, + static fn (IntegerRangeType $a, IntegerRangeType $b): int => ($a->getMin() ?? PHP_INT_MIN) <=> ($b->getMin() ?? PHP_INT_MIN) + ?: ($a->getMax() ?? PHP_INT_MAX) <=> ($b->getMax() ?? PHP_INT_MAX) + ); + $types = array_merge($types, $integerRangeTypes); $types = array_values($types); $typesCount = count($types); @@ -360,7 +381,7 @@ public static function union(Type ...$types): Type return $benevolentUnionObject->withTypes($types); } - return new BenevolentUnionType($types); + return new BenevolentUnionType($types, true); } } @@ -449,7 +470,10 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array && ($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 ( @@ -458,7 +482,10 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array && ($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 ( @@ -466,10 +493,11 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array && $a->getValue() === '0' && $b->describe(VerbosityLevel::value()) === 'non-falsy-string' ) { - return [null, new IntersectionType([ + return [null, self::intersect( new StringType(), new AccessoryNonEmptyStringType(), - ])]; + ...self::getAccessoryCaseStringTypes($b), + )]; } if ( @@ -477,15 +505,32 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array && $b->getValue() === '0' && $a->describe(VerbosityLevel::value()) === 'non-falsy-string' ) { - return [new IntersectionType([ + return [self::intersect( new StringType(), new AccessoryNonEmptyStringType(), - ]), null]; + ...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, @@ -499,6 +544,11 @@ private static function unionWithSubtractedType( $subtractedType = $type->getSubtractedType() === null ? $subtractedType : self::union($type->getSubtractedType(), $subtractedType); + + $subtractedType = self::intersect( + $type->getTypeWithoutSubtractedType(), + $subtractedType, + ); if ($subtractedType instanceof NeverType) { $subtractedType = null; } @@ -551,17 +601,31 @@ private static function intersectWithSubtractedType( } $subtractedType = self::union(...$subtractedTypes); - } 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(); + $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 = new MixedType(false, $b); } $subtractedType = self::intersect( @@ -581,8 +645,11 @@ private static function intersectWithSubtractedType( */ private static function processArrayAccessoryTypes(array $arrayTypes): array { + $isIterableAtLeastOnce = []; $accessoryTypes = []; foreach ($arrayTypes as $i => $arrayType) { + $isIterableAtLeastOnce[] = $arrayType->isIterableAtLeastOnce(); + if ($arrayType instanceof IntersectionType) { foreach ($arrayType->getTypes() as $innerType) { if ($innerType instanceof TemplateType) { @@ -612,7 +679,7 @@ private static function processArrayAccessoryTypes(array $arrayTypes): array $constantArrays = $arrayType->getConstantArrays(); foreach ($constantArrays as $constantArray) { - if ($constantArray->isList()->yes() && AccessoryArrayListType::isListTypeEnabled()) { + if ($constantArray->isList()->yes()) { $list = new AccessoryArrayListType(); $accessoryTypes[$list->describe(VerbosityLevel::cache())][$i] = $list; } @@ -645,6 +712,10 @@ private static function processArrayAccessoryTypes(array $arrayTypes): array $commonAccessoryTypes[] = $accessoryType[0]; } + if (TrinaryLogic::createYes()->and(...$isIterableAtLeastOnce)->yes()) { + $commonAccessoryTypes[] = new NonEmptyArrayType(); + } + return $commonAccessoryTypes; } @@ -747,6 +818,7 @@ private static function processArrayTypes(array $arrayTypes): array $templateArray->getVariance(), $templateArray->getName(), $arrayType, + $templateArray->getDefault(), ); } @@ -776,8 +848,10 @@ private static function optimizeConstantArrays(array $types): array } $results = []; + $eachIsOversized = true; foreach ($types as $type) { - $results[] = TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + $isOversized = false; + $result = TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$isOversized): Type { if (!$type instanceof ConstantArrayType) { return $traverse($type); } @@ -786,6 +860,8 @@ private static function optimizeConstantArrays(array $types): array return $type; } + $isOversized = true; + $isList = true; $valueTypes = []; $keyTypes = []; @@ -804,11 +880,15 @@ private static function optimizeConstantArrays(array $types): array $keyTypes[$generalizedKeyType->describe(VerbosityLevel::precise())] = $generalizedKeyType; $innerValueType = $type->getValueTypes()[$i]; - $generalizedValueType = TypeTraverser::map($innerValueType, static function (Type $type, callable $innerTraverse) use ($traverse): Type { - if ($type instanceof ArrayType) { + $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; @@ -819,11 +899,45 @@ private static function optimizeConstantArrays(array $types): array $arrayType = new ArrayType($keyType, $valueType); if ($isList) { - $arrayType = AccessoryArrayListType::intersectWith($arrayType); + $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType()); } return TypeCombinator::intersect($arrayType, new NonEmptyArrayType(), new OversizedArrayType()); }); + + if (!$isOversized) { + $eachIsOversized = false; + } + + $results[] = $result; + } + + 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()); + } + + $arrayType = new ArrayType($keyType, $valueType); + if ($eachIsList) { + $arrayType = self::intersect($arrayType, new AccessoryArrayListType()); + } + + return [self::intersect($arrayType, new NonEmptyArrayType(), new OversizedArrayType())]; } return $results; @@ -1012,6 +1126,7 @@ public static function intersect(Type ...$types): Type $union, $type->getVariance(), $type->getStrategy(), + $type->getDefault(), ); } @@ -1049,7 +1164,7 @@ public static function intersect(Type ...$types): Type } usort($types, static function (Type $a, Type $b): int { - // move subtractables with subtracts before those without to avoid loosing them in the union logic + // move subtractables with subtracts before those without to avoid losing them in the union logic if ($a instanceof SubtractableType && $a->getSubtractedType() !== null) { return -1; } @@ -1152,6 +1267,30 @@ public static function intersect(Type ...$types): Type 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(); @@ -1205,7 +1344,7 @@ public static function intersect(Type ...$types): Type continue 2; } - if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof ArrayType) { + 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) { @@ -1221,7 +1360,7 @@ public static function intersect(Type ...$types): Type continue 2; } - if ($types[$j] instanceof ConstantArrayType && $types[$i] instanceof ArrayType) { + 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) { @@ -1238,8 +1377,8 @@ public static function intersect(Type ...$types): Type } if ( - ($types[$i] instanceof ArrayType || $types[$i] instanceof IterableType) && - ($types[$j] instanceof ArrayType || $types[$j] instanceof IterableType) + ($types[$i] instanceof ArrayType || $types[$i] instanceof ConstantArrayType || $types[$i] instanceof IterableType) && + ($types[$j] instanceof ArrayType || $types[$j] instanceof ConstantArrayType || $types[$j] instanceof IterableType) ) { $keyType = self::intersect($types[$i]->getIterableKeyType(), $types[$j]->getKeyType()); $itemType = self::intersect($types[$i]->getItemType(), $types[$j]->getItemType()); @@ -1302,4 +1441,9 @@ 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 c16822e0a4..d649cfe0a7 100644 --- a/src/Type/TypeUtils.php +++ b/src/Type/TypeUtils.php @@ -6,105 +6,19 @@ use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\TemplateBenevolentUnionType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateUnionType; use function array_merge; -use function array_unique; -use function array_values; -/** @api */ -class TypeUtils +/** + * @api + */ +final class TypeUtils { /** - * @return ArrayType[] - * - * @deprecated Use PHPStan\Type\Type::getArrays() instead and handle optional ConstantArrayType keys if necessary. - */ - public static function getArrays(Type $type): array - { - if ($type instanceof ConstantArrayType) { - return $type->getAllArrays(); - } - - if ($type instanceof ArrayType) { - return [$type]; - } - - if ($type instanceof UnionType) { - $matchingTypes = []; - foreach ($type->getTypes() as $innerType) { - if (!$innerType instanceof ArrayType) { - return []; - } - foreach (self::getArrays($innerType) as $innerInnerType) { - $matchingTypes[] = $innerInnerType; - } - } - - return $matchingTypes; - } - - if ($type instanceof IntersectionType) { - $matchingTypes = []; - foreach ($type->getTypes() as $innerType) { - if (!$innerType instanceof ArrayType) { - continue; - } - foreach (self::getArrays($innerType) as $innerInnerType) { - $matchingTypes[] = $innerInnerType; - } - } - - return $matchingTypes; - } - - return []; - } - - /** - * @return ConstantArrayType[] - * - * @deprecated Use PHPStan\Type\Type::getConstantArrays() instead and handle optional keys if necessary. - */ - public static function getConstantArrays(Type $type): array - { - if ($type instanceof ConstantArrayType) { - return $type->getAllArrays(); - } - - if ($type instanceof UnionType) { - $matchingTypes = []; - foreach ($type->getTypes() as $innerType) { - if (!$innerType instanceof ConstantArrayType) { - return []; - } - foreach (self::getConstantArrays($innerType) as $innerInnerType) { - $matchingTypes[] = $innerInnerType; - } - } - - return $matchingTypes; - } - - return []; - } - - /** - * @return ConstantStringType[] - * - * @deprecated Use PHPStan\Type\Type::getConstantStrings() instead - */ - public static function getConstantStrings(Type $type): array - { - return self::map(ConstantStringType::class, $type, false); - } - - /** - * @return ConstantIntegerType[] + * @return list */ public static function getConstantIntegers(Type $type): array { @@ -112,68 +26,7 @@ public static function getConstantIntegers(Type $type): array } /** - * @deprecated Use Type::isConstantValue() or Type::generalize() - * @return ConstantType[] - */ - public static function getConstantTypes(Type $type): array - { - return self::map(ConstantType::class, $type, false); - } - - /** - * @deprecated Use Type::isConstantValue() or Type::generalize() - * @return ConstantType[] - */ - public static function getAnyConstantTypes(Type $type): array - { - return self::map(ConstantType::class, $type, false, false); - } - - /** - * @return ArrayType[] - * - * @deprecated Use PHPStan\Type\Type::getArrays() instead. - */ - public static function getAnyArrays(Type $type): array - { - return self::map(ArrayType::class, $type, true, false); - } - - /** - * @deprecated Use PHPStan\Type\Type::generalize() instead. - */ - public static function generalizeType(Type $type, GeneralizePrecision $precision): Type - { - return $type->generalize($precision); - } - - /** - * @return list - * - * @deprecated Use Type::getObjectClassNames() instead. - */ - 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) { - foreach (self::getDirectClassNames($innerType) as $n) { - $classNames[] = $n; - } - } - - return array_values(array_unique($classNames)); - } - - return []; - } - - /** - * @return IntegerRangeType[] + * @return list */ public static function getIntegerRanges(Type $type): array { @@ -181,36 +34,7 @@ public static function getIntegerRanges(Type $type): array } /** - * @deprecated Use Type::isConstantScalarValue() or Type::getConstantScalarTypes() or Type::getConstantScalarValues() - * @return ConstantScalarType[] - */ - public static function getConstantScalars(Type $type): array - { - return self::map(ConstantScalarType::class, $type, false); - } - - /** - * @deprecated Use Type::getEnumCases() - * @return EnumCaseObjectType[] - */ - public static function getEnumCaseObjects(Type $type): array - { - return self::map(EnumCaseObjectType::class, $type, false); - } - - /** - * @internal - * @return ConstantArrayType[] - * - * @deprecated Use PHPStan\Type\Type::getConstantArrays(). - */ - public static function getOldConstantArrays(Type $type): array - { - return self::map(ConstantArrayType::class, $type, false); - } - - /** - * @return mixed[] + * @return list */ private static function map( string $typeClass, @@ -289,6 +113,7 @@ public static function toStrictUnion(Type $type): Type $type->getVariance(), $type->getName(), static::toStrictUnion($type->getBound()), + $type->getDefault(), ); } @@ -345,6 +170,24 @@ 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; + } + /** * @return HasPropertyType[] */ @@ -374,24 +217,6 @@ public static function getAccessoryTypes(Type $type): array return self::map(AccessoryType::class, $type, true, false); } - /** @deprecated Use PHPStan\Type\Type::isCallable() instead. */ - public static function containsCallable(Type $type): bool - { - if ($type->isCallable()->yes()) { - return true; - } - - if ($type instanceof UnionType) { - foreach ($type->getTypes() as $innerType) { - if ($innerType->isCallable()->yes()) { - return true; - } - } - } - - return false; - } - public static function containsTemplateType(Type $type): bool { $containsTemplateType = false; @@ -408,11 +233,22 @@ public static function containsTemplateType(Type $type): bool public static function resolveLateResolvableTypes(Type $type, bool $resolveUnresolvableTypes = true): Type { - return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($resolveUnresolvableTypes): Type { - while ($type instanceof LateResolvableType && ($resolveUnresolvableTypes || $type->isResolvable())) { + /** @var int $ignoreResolveUnresolvableTypesLevel */ + $ignoreResolveUnresolvableTypesLevel = 0; + + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($resolveUnresolvableTypes, &$ignoreResolveUnresolvableTypesLevel): Type { + while ($type instanceof LateResolvableType && (($resolveUnresolvableTypes && $ignoreResolveUnresolvableTypesLevel === 0) || $type->isResolvable())) { $type = $type->resolve(); } + if ($type instanceof CallableType || $type instanceof ClosureType) { + $ignoreResolveUnresolvableTypesLevel++; + $result = $traverse($type); + $ignoreResolveUnresolvableTypesLevel--; + + return $result; + } + return $traverse($type); }); } diff --git a/src/Type/TypehintHelper.php b/src/Type/TypehintHelper.php index 09e33f5c9b..41c85112a7 100644 --- a/src/Type/TypehintHelper.php +++ b/src/Type/TypehintHelper.php @@ -2,113 +2,41 @@ namespace PHPStan\Type; +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\Reflection\ReflectionProviderStaticAccessor; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Generic\TemplateTypeHelper; -use ReflectionIntersectionType; -use ReflectionNamedType; use ReflectionType; -use ReflectionUnionType; use function array_map; use function count; use function get_class; -use function is_string; use function sprintf; -use function str_ends_with; -use function strtolower; -class TypehintHelper +final class TypehintHelper { - private static function getTypeObjectFromTypehint(string $typeString, ClassReflection|string|null $selfClass): Type - { - switch (strtolower($typeString)) { - case 'int': - return new IntegerType(); - case 'bool': - return new BooleanType(); - case 'false': - return new ConstantBooleanType(false); - case 'true': - return new ConstantBooleanType(true); - 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': - if ($selfClass instanceof ClassReflection) { - $selfClass = $selfClass->getName(); - } - return $selfClass !== null ? new ObjectType($selfClass) : new ErrorType(); - case 'parent': - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if (is_string($selfClass)) { - if ($reflectionProvider->hasClass($selfClass)) { - $selfClass = $reflectionProvider->getClass($selfClass); - } else { - $selfClass = null; - } - } - if ($selfClass !== null) { - if ($selfClass->getParentClass() !== null) { - return new ObjectType($selfClass->getParentClass()->getName()); - } - } - return new NonexistentParentClassType(); - case 'static': - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if (is_string($selfClass)) { - if ($reflectionProvider->hasClass($selfClass)) { - $selfClass = $reflectionProvider->getClass($selfClass); - } else { - $selfClass = null; - } - } - if ($selfClass !== null) { - return new StaticType($selfClass); - } - - return new ErrorType(); - case 'null': - return new NullType(); - case 'never': - return new NonAcceptingNeverType(); - default: - return new ObjectType($typeString); - } - } - /** @api */ public static function decideTypeFromReflection( ?ReflectionType $reflectionType, ?Type $phpDocType = null, - ClassReflection|string|null $selfClass = null, + ClassReflection|null $selfClass = null, bool $isVariadic = false, ): Type { if ($reflectionType === null) { - if ($isVariadic && $phpDocType instanceof ArrayType) { + if ($isVariadic && ($phpDocType instanceof ArrayType || $phpDocType instanceof ConstantArrayType)) { $phpDocType = $phpDocType->getItemType(); } return $phpDocType ?? new MixedType(); } if ($reflectionType instanceof ReflectionUnionType) { - $type = TypeCombinator::union(...array_map(static fn (ReflectionType $type): Type => 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); } @@ -116,7 +44,7 @@ public static function decideTypeFromReflection( if ($reflectionType instanceof ReflectionIntersectionType) { $types = []; foreach ($reflectionType->getTypes() as $innerReflectionType) { - $innerType = self::decideTypeFromReflection($innerReflectionType, null, $selfClass, false); + $innerType = self::decideTypeFromReflection($innerReflectionType, selfClass: $selfClass); if (!$innerType->isObject()->yes()) { return new NeverType(); } @@ -131,27 +59,15 @@ public static function decideTypeFromReflection( throw new ShouldNotHappenException(sprintf('Unexpected type: %s', get_class($reflectionType))); } - $reflectionTypeString = $reflectionType->getName(); - $loweredReflectionTypeString = strtolower($reflectionTypeString); - if (str_ends_with($loweredReflectionTypeString, '\\object')) { - $reflectionTypeString = 'object'; - } elseif (str_ends_with($loweredReflectionTypeString, '\\mixed')) { - $reflectionTypeString = 'mixed'; - } elseif (str_ends_with($loweredReflectionTypeString, '\\true')) { - $reflectionTypeString = 'true'; - } elseif (str_ends_with($loweredReflectionTypeString, '\\false')) { - $reflectionTypeString = 'false'; - } elseif (str_ends_with($loweredReflectionTypeString, '\\null')) { - $reflectionTypeString = 'null'; - } elseif (str_ends_with($loweredReflectionTypeString, '\\never')) { - $reflectionTypeString = 'never'; + if ($reflectionType->isIdentifier()) { + $typeNode = new Identifier($reflectionType->getName()); + } else { + $typeNode = new FullyQualified($reflectionType->getName()); } - $type = self::getTypeObjectFromTypehint($reflectionTypeString, $selfClass); + $type = ParserNodeTypeToPHPStanType::resolve($typeNode, $selfClass); if ($reflectionType->allowsNull()) { $type = TypeCombinator::addNull($type); - } elseif ($phpDocType !== null) { - $phpDocType = TypeCombinator::removeNull($phpDocType); } return self::decideType($type, $phpDocType); @@ -159,9 +75,12 @@ 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; } @@ -182,7 +101,7 @@ public static function decideType( if ($phpDocType instanceof UnionType) { $innerTypes = []; foreach ($phpDocType->getTypes() as $innerType) { - if ($innerType instanceof ArrayType) { + if ($innerType instanceof ArrayType || $innerType instanceof ConstantArrayType) { $innerTypes[] = new IterableType( $innerType->getIterableKeyType(), $innerType->getItemType(), @@ -192,7 +111,7 @@ public static function decideType( } } $phpDocType = new UnionType($innerTypes); - } elseif ($phpDocType instanceof ArrayType) { + } elseif ($phpDocType instanceof ArrayType || $phpDocType instanceof ConstantArrayType) { $phpDocType = new IterableType( $phpDocType->getKeyType(), $phpDocType->getItemType(), @@ -201,8 +120,11 @@ public static function decideType( } if ( - (!$phpDocType instanceof NeverType || ($type instanceof MixedType && !$type->isExplicitMixed())) - && $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType))->yes() + ($type->isCallable()->yes() && $phpDocType->isCallable()->yes()) + || ( + (!$phpDocType instanceof NeverType || ($type instanceof MixedType && !$type->isExplicitMixed())) + && $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType))->yes() + ) ) { $resultType = $phpDocType; } else { diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 1a8ac35969..f459041746 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -5,14 +5,16 @@ 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\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\Type\UnionTypeUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnionTypeUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; @@ -20,12 +22,14 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; 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; @@ -45,6 +49,11 @@ class UnionType implements CompoundType 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 */ @@ -86,6 +95,29 @@ public function getTypes(): array return $this->types; } + /** + * @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; @@ -106,9 +138,6 @@ protected function getSortedTypes(): array return $this->types; } - /** - * @return string[] - */ public function getReferencedClasses(): array { $classes = []; @@ -161,54 +190,53 @@ public function getConstantStrings(): array ); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { - return $this->acceptsWithReason($type, $strictTypes)->result; - } + foreach (self::EQUAL_UNION_CLASSES as $baseClass => $classes) { + if (!$type->equals(new ObjectType($baseClass))) { + continue; + } - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult - { - if ( - $type->equals(new ObjectType(DateTimeInterface::class)) - && $this->accepts( - new UnionType([new ObjectType(DateTime::class), new ObjectType(DateTimeImmutable::class)]), - $strictTypes, - )->yes() - ) { - return AcceptsResult::createYes(); + $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->acceptsWithReason($type, $strictTypes)->decorateReasons(static fn (string $reason) => sprintf('Type #%d from the union: %s', $i + 1, $reason))); + $result = $result->or($innerType->accepts($type, $strictTypes)->decorateReasons(static fn (string $reason) => sprintf('Type #%d from the union: %s', $i + 1, $reason))); } if ($result->yes()) { return $result; } if ($type instanceof CompoundType && !$type instanceof CallableType && !$type instanceof TemplateType && !$type instanceof IntersectionType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } if ($type instanceof TemplateUnionType) { - return $result->or($type->isAcceptedWithReasonBy($this, $strictTypes)); + return $result->or($type->isAcceptedBy($this, $strictTypes)); } if ($type->isEnum()->yes() && !$this->isEnum()->no()) { $enumCasesUnion = TypeCombinator::union(...$type->getEnumCases()); if (!$type->equals($enumCasesUnion)) { - return $this->acceptsWithReason($enumCasesUnion, $strictTypes); + return $this->accepts($enumCasesUnion, $strictTypes); } } return $result; } - public function isSuperTypeOf(Type $otherType): TrinaryLogic + public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult { if ( ($otherType instanceof self && !$otherType instanceof TemplateUnionType) - || $otherType instanceof IterableType + || ($otherType instanceof IterableType && !$otherType instanceof TemplateIterableType) || $otherType instanceof NeverType || $otherType instanceof ConditionalType || $otherType instanceof ConditionalTypeForParameter @@ -217,10 +245,15 @@ public function isSuperTypeOf(Type $otherType): TrinaryLogic return $otherType->isSubTypeOf($this); } - $result = TrinaryLogic::createNo()->lazyOr($this->getTypes(), static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType)); - if ($result->yes()) { - return $result; + $results = []; + foreach ($this->types as $innerType) { + $result = $innerType->isSuperTypeOf($otherType); + if ($result->yes()) { + return $result; + } + $results[] = $result; } + $result = IsSuperTypeOfResult::createNo()->or(...$results); if ($otherType instanceof TemplateUnionType) { return $result->or($otherType->isSubTypeOf($this)); @@ -229,19 +262,14 @@ public function isSuperTypeOf(Type $otherType): TrinaryLogic return $result; } - public function isSubTypeOf(Type $otherType): TrinaryLogic - { - return TrinaryLogic::lazyExtremeIdentity($this->getTypes(), static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType)); - } - - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { - return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + return IsSuperTypeOfResult::extremeIdentity(...array_map(static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType), $this->types)); } - public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return AcceptsResult::extremeIdentity(...array_map(static fn (Type $innerType) => $acceptingType->acceptsWithReason($innerType, $strictTypes), $this->types)); + return AcceptsResult::extremeIdentity(...array_map(static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes), $this->types)); } public function equals(Type $type): bool @@ -309,7 +337,7 @@ public function describe(VerbosityLevel $level): string } } - if ($level->isPrecise()) { + if ($level->isPrecise() || $level->isCache()) { $duplicates = array_diff_assoc($typeNames, array_unique($typeNames)); if (count($duplicates) > 0) { $indexByDuplicate = array_fill_keys($duplicates, 0); @@ -435,7 +463,7 @@ public function hasProperty(string $propertyName): TrinaryLogic return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName)); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } @@ -514,11 +542,11 @@ public function hasConstant(string $constantName): TrinaryLogic ); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { return $this->getInternal( static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName), - static fn (Type $type): ConstantReflection => $type->getConstant($constantName), + static fn (Type $type): ClassConstantReflection => $type->getConstant($constantName), ); } @@ -612,9 +640,19 @@ public function isLiteralString(): TrinaryLogic return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString()); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic { - return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isClassStringType()); + 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 @@ -639,7 +677,9 @@ public function isScalar(): TrinaryLogic public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { - return new BooleanType(); + return $this->notBenevolentUnionResults( + static fn (Type $innerType): TrinaryLogic => $innerType->looseCompare($type, $phpVersion)->toTrinaryLogic() + )->toBooleanType(); } public function isOffsetAccessible(): TrinaryLogic @@ -691,6 +731,11 @@ 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()); @@ -701,6 +746,11 @@ 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)); @@ -721,9 +771,14 @@ public function popArray(): Type return $this->unionTypes(static fn (Type $type): Type => $type->popArray()); } - public function searchArray(Type $needleType): Type + 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)); + return $this->unionTypes(static fn (Type $type): Type => $type->searchArray($needleType, $strict)); } public function shiftArray(): Type @@ -736,6 +791,16 @@ 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( @@ -773,14 +838,14 @@ public function isCloneable(): TrinaryLogic return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCloneable()); } - public function isSmallerThan(Type $otherType): TrinaryLogic + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { - return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType)); + 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->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType)); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType, $phpVersion)); } public function isNull(): TrinaryLogic @@ -833,34 +898,34 @@ public function isInteger(): TrinaryLogic return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isInteger()); } - public function getSmallerType(): Type + public function getSmallerType(PhpVersion $phpVersion): Type { - return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerType()); + return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerType($phpVersion)); } - public function getSmallerOrEqualType(): Type + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { - return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType()); + return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType($phpVersion)); } - public function getGreaterType(): Type + public function getGreaterType(PhpVersion $phpVersion): Type { - return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterType()); + return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterType($phpVersion)); } - public function getGreaterOrEqualType(): Type + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { - return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType()); + return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType($phpVersion)); } - public function isGreaterThan(Type $otherType): TrinaryLogic + public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { - return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type)); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type, $phpVersion)); } - public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic + public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { - return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type)); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type, $phpVersion)); } public function toBoolean(): BooleanType @@ -918,6 +983,11 @@ 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(); @@ -1058,14 +1128,6 @@ public function getFiniteTypes(): array return array_values($uniquedTypes); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self($properties['types'], $properties['normalized']); - } - /** * @param callable(Type $type): TrinaryLogic $getResult */ @@ -1090,18 +1152,6 @@ protected function unionTypes(callable $getType): Type return TypeCombinator::union(...array_map($getType, $this->types)); } - /** - * @template T of Type - * @param callable(Type $type): list $getTypes - * @return list - * - * @deprecated Use pickFromTypes() instead. - */ - protected function pickTypes(callable $getTypes): array - { - return $this->pickFromTypes($getTypes, static fn () => false); - } - /** * @template T * @param callable(Type $type): list $getValues diff --git a/src/Type/UnionTypeHelper.php b/src/Type/UnionTypeHelper.php index a2c28b3153..f91ea5cb45 100644 --- a/src/Type/UnionTypeHelper.php +++ b/src/Type/UnionTypeHelper.php @@ -12,7 +12,7 @@ use function usort; use const PHP_INT_MIN; -class UnionTypeHelper +final class UnionTypeHelper { /** @@ -116,6 +116,10 @@ public static function sortTypes(array $types): array return self::compareStrings($a->describe(VerbosityLevel::value()), $b->describe(VerbosityLevel::value())); } + if ($a->isString()->yes() && $b->isString()->yes()) { + return self::compareStrings($a->describe(VerbosityLevel::precise()), $b->describe(VerbosityLevel::precise())); + } + return self::compareStrings($a->describe(VerbosityLevel::typeOnly()), $b->describe(VerbosityLevel::typeOnly())); }); return $types; diff --git a/src/Type/UsefulTypeAliasResolver.php b/src/Type/UsefulTypeAliasResolver.php index 191bad9794..6577289ff9 100644 --- a/src/Type/UsefulTypeAliasResolver.php +++ b/src/Type/UsefulTypeAliasResolver.php @@ -3,6 +3,8 @@ namespace PHPStan\Type; use PHPStan\Analyser\NameScope; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\ReflectionProvider; @@ -10,7 +12,8 @@ use function array_key_exists; use function sprintf; -class UsefulTypeAliasResolver implements TypeAliasResolver +#[AutowiredService(as: TypeAliasResolver::class)] +final class UsefulTypeAliasResolver implements TypeAliasResolver { /** @var array */ @@ -29,6 +32,7 @@ class UsefulTypeAliasResolver implements TypeAliasResolver * @param array $globalTypeAliases */ public function __construct( + #[AutowiredParameter(ref: '%typeAliases%')] private array $globalTypeAliases, private TypeStringResolver $typeStringResolver, private TypeNodeResolver $typeNodeResolver, diff --git a/src/Type/ValueOfType.php b/src/Type/ValueOfType.php index 9df5411c47..d02b7d11f7 100644 --- a/src/Type/ValueOfType.php +++ b/src/Type/ValueOfType.php @@ -100,14 +100,4 @@ public function toPhpDocNode(): TypeNode return new GenericTypeNode(new IdentifierTypeNode('value-of'), [$this->type->toPhpDocNode()]); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self( - $properties['type'], - ); - } - } diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index c80bcbce99..59259d2df7 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -4,14 +4,17 @@ 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; @@ -83,16 +86,29 @@ 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->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 ( @@ -107,6 +123,14 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc $moreVerbose = true; return $type; } + if ( + $type instanceof AccessoryLowercaseStringType + || $type instanceof AccessoryUppercaseStringType + ) { + $moreVerbose = true; + $veryVerbose = true; + return $type; + } if ($type instanceof IntegerRangeType) { $moreVerbose = true; return $type; @@ -116,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(); @@ -151,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(); } /** diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index 8cbd1076ba..83c5a05726 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -37,9 +37,6 @@ public function __construct() { } - /** - * @return string[] - */ public function getReferencedClasses(): array { return []; @@ -55,31 +52,26 @@ public function getObjectClassReflections(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->acceptsWithReason($type, $strictTypes)->result; - } - - public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return new AcceptsResult($type->isVoid()->or($type->isNull()), []); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool @@ -127,6 +119,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function toCoercedArgumentType(bool $strictTypes): Type + { + return new NullType(); + } + public function isOffsetAccessLegal(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -207,7 +204,17 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isClassStringType(): TrinaryLogic + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -262,12 +269,4 @@ public function toPhpDocNode(): TypeNode return new IdentifierTypeNode('void'); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self(); - } - } 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 @@ + */ public $name; @@ -42,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 index 20396c04fc..6a13532d9a 100644 --- a/stubs/ReflectionEnum.stub +++ b/stubs/ReflectionEnum.stub @@ -19,7 +19,7 @@ class ReflectionEnum extends ReflectionClass public function getCase(string $name): ReflectionEnumUnitCase {} /** - * @phpstan-assert-if-true self $this + * @phpstan-assert-if-true self $this * @phpstan-assert-if-true !null $this->getBackingType() */ public function isBacked(): bool {} diff --git a/stubs/ReflectionEnumWithLazyObjects.stub b/stubs/ReflectionEnumWithLazyObjects.stub new file mode 100644 index 0000000000..3955a5013f --- /dev/null +++ b/stubs/ReflectionEnumWithLazyObjects.stub @@ -0,0 +1,27 @@ + + */ +class ReflectionEnum extends ReflectionClass +{ + + /** + * @return (T is BackedEnum ? ReflectionEnumBackedCase[] : ReflectionEnumUnitCase[]) + */ + public function getCases(): array {} + + /** + * @return (T is BackedEnum ? ReflectionEnumBackedCase : ReflectionEnumUnitCase) + * @throws ReflectionException + */ + public function getCase(string $name): ReflectionEnumUnitCase {} + + /** + * @phpstan-assert-if-true self $this + * @phpstan-assert-if-true !null $this->getBackingType() + */ + public function isBacked(): bool {} + +} diff --git a/stubs/ReflectionFunctionAbstract.stub b/stubs/ReflectionFunctionAbstract.stub index 36e48df100..0154996741 100644 --- a/stubs/ReflectionFunctionAbstract.stub +++ b/stubs/ReflectionFunctionAbstract.stub @@ -9,7 +9,7 @@ abstract class ReflectionFunctionAbstract public function getFileName () {} /** - * @return array> + * @return list> */ public function getAttributes(?string $name = null, int $flags = 0) { diff --git a/stubs/ReflectionParameter.stub b/stubs/ReflectionParameter.stub index a29fa35e83..e92f0b51f3 100644 --- a/stubs/ReflectionParameter.stub +++ b/stubs/ReflectionParameter.stub @@ -3,7 +3,7 @@ class ReflectionParameter { /** - * @return array> + * @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 3f7c44f120..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 { /** diff --git a/stubs/arrayFunctions.stub b/stubs/arrayFunctions.stub index 25c73249ec..3297e47316 100644 --- a/stubs/arrayFunctions.stub +++ b/stubs/arrayFunctions.stub @@ -16,6 +16,14 @@ function array_reduce( $three = null ) {} +/** + * @template T of mixed + * + * @param array $array + * @return ($array is non-empty-array ? non-empty-list : list) + */ +function array_values(array $array): array {} + /** * @template TKey as (int|string) * @template T @@ -50,20 +58,163 @@ function uksort(array &$array, callable $callback): bool } /** - * @template T of mixed + * @template TV of mixed + * @template TK of mixed * - * @param array $one - * @param array $two - * @param callable(T, T): int $three + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @return array */ function array_udiff( array $one, array $two, callable $three -): int {} +): array {} /** * @param array $value - * @return ($value is __always-list ? true : false) + * @return ($value is list ? true : false) */ function array_is_list(array $value): bool {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TK, TK): int $three + * @return array + */ +function array_diff_uassoc( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TK, TK): int $three + * @return array + */ +function array_diff_ukey( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TK, TK): int $three + * @return array + */ +function array_intersect_uassoc( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TK, TK): int $three + * + * @return array + */ +function array_intersect_ukey( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * + * @return array + */ +function array_udiff_assoc( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @param callable(TK, TK): int $four + * @return array + */ +function array_udiff_uassoc( + array $one, + array $two, + callable $three, + callable $four +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @return array + */ +function array_uintersect_assoc( + array $one, + array $two, + callable $three, +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @param callable(TK, TK): int $four + * @return array + */ +function array_uintersect_uassoc( + array $one, + array $two, + callable $three, + callable $four +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @return array + */ +function array_uintersect( + array $one, + array $two, + callable $three, +): array {} diff --git a/stubs/bleedingEdge/Countable.stub b/stubs/bleedingEdge/Countable.stub deleted file mode 100644 index 2491db1ce5..0000000000 --- a/stubs/bleedingEdge/Countable.stub +++ /dev/null @@ -1,9 +0,0 @@ - - */ - public function getNodeType(): string; - - /** - * @phpstan-param TNodeType $node - * @return list - */ - public function processNode(Node $node, Scope $scope): array; - -} diff --git a/stubs/core.stub b/stubs/core.stub index 2cb6f29448..6456c2d1b9 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -35,12 +35,6 @@ function highlight_file($var, bool $return = false) {} */ function show_source($var, bool $return = false) {} -/** - * @param mixed $var - * @return ($return is true ? string : bool) - */ -function highlight_string($var, bool $return = false) {} - /** * @param mixed $var * @param bool $return @@ -81,12 +75,12 @@ function parse_str(string $string, array &$result): void {} /** * @param array $result - * @param-out array|string> $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 {} +function similar_text(string $string1, string $string2, ?float &$percent = null) : int {} /** * @param mixed $output @@ -228,9 +222,9 @@ function preg_match_all($pattern, $subject, &$matches = [], int $flags = 1, int function preg_match($pattern, $subject, &$matches = [], int $flags = 0, int $offset = 0) {} /** - * @param string|array $pattern - * @param callable(array):string $callback - * @param string|array $subject + * @param string|string[] $pattern + * @param callable(string[]):string $callback + * @param string|array $subject * @param int $count * @param-out 0|positive-int $count * @return ($subject is array ? list|null : string|null) @@ -238,9 +232,9 @@ function preg_match($pattern, $subject, &$matches = [], int $flags = 0, int $off function preg_replace_callback($pattern, $callback, $subject, int $limit = -1, &$count = null, int $flags = 0) {} /** - * @param string|array $pattern - * @param string|array $replacement - * @param string|array $subject + * @param string|string[] $pattern + * @param string|array $replacement + * @param string|array $subject * @param int $count * @param-out 0|positive-int $count * @return ($subject is array ? list|null : string|null) @@ -248,9 +242,9 @@ function preg_replace_callback($pattern, $callback, $subject, int $limit = -1, & function preg_replace($pattern, $replacement, $subject, int $limit = -1, &$count = null) {} /** - * @param string|array $pattern - * @param string|array $replacement - * @param string|array $subject + * @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) @@ -258,22 +252,22 @@ function preg_replace($pattern, $replacement, $subject, int $limit = -1, &$count function preg_filter($pattern, $replacement, $subject, int $limit = -1, &$count = null) {} /** - * @param array|string $search - * @param array|string $replace - * @param array|string $subject + * @param array|string $search + * @param array|string $replace + * @param array|string $subject * @param-out int $count * @return list|string */ -function str_replace($search, $replace, $subject, int &$count = null) {} +function str_replace($search, $replace, $subject, ?int &$count = null) {} /** - * @param array|string $search - * @param array|string $replace - * @param array|string $subject + * @param array|string $search + * @param array|string $replace + * @param array|string $subject * @param-out int $count * @return list|string */ -function str_ireplace($search, $replace, $subject, int &$count = null) {} +function str_ireplace($search, $replace, $subject, ?int &$count = null) {} /** * @template TRead of null|array @@ -300,16 +294,71 @@ function flock($stream, int $operation, mixed &$would_block = null): bool {} * @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) {} +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 {} +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 {} +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 {} diff --git a/stubs/dom.stub b/stubs/dom.stub index d2a5c575fc..074bdd5102 100644 --- a/stubs/dom.stub +++ b/stubs/dom.stub @@ -30,11 +30,25 @@ 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 DOMNamedNodeMap */ + public $attributes; + /** @var DOMDocument */ public $ownerDocument; diff --git a/stubs/ext-ds.stub b/stubs/ext-ds.stub index 05fdf38f0a..03acd69af5 100644 --- a/stubs/ext-ds.stub +++ b/stubs/ext-ds.stub @@ -61,7 +61,7 @@ final class Deque implements Sequence * @param (callable(TValue): bool)|null $callback * @return Deque */ - public function filter(callable $callback = null): Deque + public function filter(?callable $callback = null): Deque { } @@ -81,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 */ @@ -190,7 +198,7 @@ final class Map implements Collection, ArrayAccess * @param (callable(TKey, TValue): bool)|null $callback * @return Map */ - public function filter(callable $callback = null): Map + public function filter(?callable $callback = null): Map { } @@ -284,7 +292,7 @@ final class Map implements Collection, ArrayAccess * @param (callable(TValue, TValue): int)|null $comparator * @return void */ - public function sort(callable $comparator = null) + public function sort(?callable $comparator = null) { } @@ -292,7 +300,7 @@ final class Map implements Collection, ArrayAccess * @param (callable(TValue, TValue): int)|null $comparator * @return Map */ - public function sorted(callable $comparator = null): Map + public function sorted(?callable $comparator = null): Map { } @@ -300,7 +308,7 @@ final class Map implements Collection, ArrayAccess * @param (callable(TKey, TKey): int)|null $comparator * @return void */ - public function ksort(callable $comparator = null) + public function ksort(?callable $comparator = null) { } @@ -308,7 +316,7 @@ final class Map implements Collection, ArrayAccess * @param (callable(TKey, TKey): int)|null $comparator * @return Map */ - public function ksorted(callable $comparator = null): Map + public function ksorted(?callable $comparator = null): Map { } @@ -348,8 +356,8 @@ final class Map implements Collection, ArrayAccess } /** - * @template-covariant TKey - * @template-covariant TValue + * @template TKey + * @template TValue */ final class Pair implements JsonSerializable { @@ -401,7 +409,7 @@ interface Sequence extends Collection, ArrayAccess * @param (callable(TValue): bool)|null $callback * @return Sequence */ - public function filter(callable $callback = null); + public function filter(?callable $callback = null); /** * @param TValue $value @@ -432,7 +440,7 @@ interface Sequence extends Collection, ArrayAccess * @param string $glue * @return string */ - public function join(string $glue = null): string; + public function join(?string $glue = null): string; /** * @return TValue @@ -509,13 +517,13 @@ interface Sequence extends Collection, ArrayAccess * @param (callable(TValue, TValue): int)|null $comparator * @return void */ - public function sort(callable $comparator = null); + public function sort(?callable $comparator = null); /** * @param (callable(TValue, TValue): int)|null $comparator * @return Sequence */ - public function sorted(callable $comparator = null); + public function sorted(?callable $comparator = null); /** * @param TValue ...$values @@ -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 { } @@ -642,7 +650,7 @@ final class Set implements Collection, ArrayAccess * @param (callable(TValue): bool)|null $callback * @return Set */ - public function filter(callable $callback = null): Set + public function filter(?callable $callback = null): Set { } @@ -731,7 +739,7 @@ final class Set implements Collection, ArrayAccess /** * @param (callable(TValue, TValue): int)|null $comparator */ - public function sort(callable $comparator = null): void + public function sort(?callable $comparator = null): void { } @@ -739,7 +747,7 @@ final class Set implements Collection, ArrayAccess * @param (callable(TValue, TValue): int)|null $comparator * @return Set */ - public function sorted(callable $comparator = null): Set + public function sorted(?callable $comparator = null): Set { } diff --git a/stubs/ibm_db2.stub b/stubs/ibm_db2.stub index 1b0e578bfc..544473f615 100644 --- a/stubs/ibm_db2.stub +++ b/stubs/ibm_db2.stub @@ -6,4 +6,4 @@ * * @return ($value is null ? \DB2_AUTOCOMMIT_OFF|\DB2_AUTOCOMMIT_ON : bool) */ -function db2_autocommit($connection, int $value = null) {} +function db2_autocommit($connection, ?int $value = null) {} diff --git a/stubs/runtime/Attribute.php b/stubs/runtime/Attribute84.php similarity index 100% rename from stubs/runtime/Attribute.php rename to stubs/runtime/Attribute84.php 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/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 b84ff5feb0..dfc11a9947 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -7,13 +7,12 @@ use ExtendingKnownClassWithCheck\Foo; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\Reflection\ParametersAcceptorSelector; 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 restore_error_handler; use function sprintf; use const PHP_VERSION_ID; @@ -82,7 +81,7 @@ public function testExtendingKnownClassWithCheck(): void $errors = $this->runAnalyse(__DIR__ . '/data/extending-known-class-with-check.php'); $this->assertNoErrors($errors); - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $this->assertTrue($reflectionProvider->hasClass(Foo::class)); } @@ -154,6 +153,12 @@ public function testExtendsPdoStatementCrash(): void $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'); @@ -178,7 +183,6 @@ public function testClassExistsAutoloadingError(): void public function testCollectWarnings(): void { - restore_error_handler(); $errors = $this->runAnalyse(__DIR__ . '/data/declaration-warning.php'); $this->assertCount(1, $errors); $this->assertSame('Parameter #1 $i of method DeclarationWarning\Bar::doFoo() is not optional.', $errors[0]->getMessage()); @@ -261,6 +265,13 @@ public function testBug3686(): void $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 { $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-3379.php'); @@ -312,12 +323,15 @@ public function testBug3309(): void $this->assertNoErrors($errors); } - public function testBug6872(): void + public function testBug11649(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } + $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); } @@ -361,9 +375,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()); @@ -374,9 +388,9 @@ public function testBug4288(): void $errors = $this->runAnalyse(__DIR__ . '/data/bug-4288.php'); $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()); @@ -411,9 +425,6 @@ public function testFunctionThatExistsOn72AndLater(): void public function testBug4715(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $errors = $this->runAnalyse(__DIR__ . '/data/bug-4715.php'); $this->assertNoErrors($errors); } @@ -440,6 +451,20 @@ 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__ . '/nsrt/bug-5529.php'); @@ -464,21 +489,16 @@ public function testBug5657(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.0')] public function testBug5951(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $errors = $this->runAnalyse(__DIR__ . '/data/bug-5951.php'); $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.1')] public function testEnums(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/enums-integration.php'); $this->assertCount(3, $errors); $this->assertSame('Access to an undefined property EnumIntegrationTest\Foo::TWO::$value.', $errors[0]->getMessage()); @@ -498,12 +518,9 @@ public function testBug6255(): void public function testBug6300(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-6300.php'); - $this->assertCount(2, $errors); + $this->assertCount(1, $errors); $this->assertSame('Call to an undefined method Bug6300\Bar::get().', $errors[0]->getMessage()); - $this->assertSame(23, $errors[0]->getLine()); - - $this->assertSame('Access to an undefined property Bug6300\Bar::$fooProp.', $errors[1]->getMessage()); - $this->assertSame(24, $errors[1]->getLine()); + $this->assertSame(27, $errors[0]->getLine()); } public function testBug6466(): void @@ -512,11 +529,9 @@ public function testBug6466(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.1')] public function testBug6494(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } $errors = $this->runAnalyse(__DIR__ . '/data/bug-6494.php'); $this->assertNoErrors($errors); } @@ -538,12 +553,18 @@ public function testBug6442(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-6442.php'); $this->assertCount(2, $errors); - $this->assertSame('Dumped type: \'Bug6442\\\A\'', $errors[0]->getMessage()); + $this->assertSame('Dumped type: \'Bug6442\\\B\'', $errors[0]->getMessage()); $this->assertSame(9, $errors[0]->getLine()); - $this->assertSame('Dumped type: \'Bug6442\\\B\'', $errors[1]->getMessage()); + $this->assertSame('Dumped type: \'Bug6442\\\A\'', $errors[1]->getMessage()); $this->assertSame(9, $errors[1]->getLine()); } + 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'); @@ -558,12 +579,9 @@ public function testBug6501(): void $this->assertSame(24, $errors[0]->getLine()); } + #[RequiresPhp('>= 8.0')] public function testBug6114(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-6114.php'); $this->assertNoErrors($errors); } @@ -598,6 +616,12 @@ public function testBug6649(): void $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'); @@ -609,12 +633,9 @@ public function testBug6842(): void $this->assertSame(54, $errors[1]->getLine()); } + #[RequiresPhp('>= 8.0')] public function testBug6896(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-6896.php'); $this->assertCount(4, $errors); $this->assertSame('Generic type IteratorIterator<(int|string), mixed> in PHPDoc tag @return does not specify all template types of class IteratorIterator: TKey, TValue, TIterator', $errors[0]->getMessage()); @@ -653,12 +674,9 @@ public function testBug1388(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.0')] public function testBug4308(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-4308.php'); $this->assertNoErrors($errors); } @@ -693,22 +711,16 @@ public function testBug7030(): void 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 { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-7012.php'); $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.1')] public function testBug6192(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-6192.php'); $this->assertNoErrors($errors); } @@ -719,12 +731,9 @@ public function testBug7068(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.0')] public function testDiscussion6993(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-6993.php'); $this->assertCount(1, $errors); $this->assertSame('Parameter #1 $specificable of method Bug6993\AndSpecificationValidator::isSatisfiedBy() expects Bug6993\Foo, Bug6993\Bar given.', $errors[0]->getMessage()); @@ -736,22 +745,16 @@ public function testBug7077(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.0')] public function testBug7078(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-7078.php'); $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.0')] public function testBug7116(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-7116.php'); $this->assertNoErrors($errors); } @@ -762,23 +765,17 @@ public function testBug3853(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.1')] public function testBug7135(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-7135.php'); $this->assertCount(1, $errors); $this->assertSame('Cannot create callable from the new operator.', $errors[0]->getMessage()); } + #[RequiresPhp('>= 8.0')] public function testDiscussion7124(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/discussion-7124.php'); $this->assertCount(4, $errors); $this->assertSame('Parameter #2 $callback of function Discussion7124\filter expects callable(bool, 0|1|2=): bool, Closure(int, bool): bool given.', $errors[0]->getMessage()); @@ -799,6 +796,15 @@ public function testBug7214(): void $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'); @@ -825,6 +831,7 @@ public function testBug7094(): void $this->assertSame(29, $errors[5]->getLine()); } + #[RequiresPhp('>= 8.0')] public function testOffsetAccess(): void { $errors = $this->runAnalyse(__DIR__ . '/nsrt/offset-access.php'); @@ -837,7 +844,7 @@ 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, array|false given.', $errors[0]->getMessage()); + $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()); @@ -881,15 +888,25 @@ public function testBug7500(): void $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, array>|false given.', PHP_VERSION_ID < 80000 ? 'var' : 'value'), $errors[0]->getMessage()); + $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('Cannot access offset int<1, max> on list}>|false.', $errors[1]->getMessage()); $this->assertSame(27, $errors[1]->getLine()); } @@ -908,6 +925,12 @@ public function testBug7637(): void $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'); @@ -976,6 +999,12 @@ public function testArrayUnion(): void $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'); @@ -988,22 +1017,16 @@ public function testBug7963Two(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.1')] public function testBug8078(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-8078.php'); $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.1')] public function testBug8072(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-8072.php'); $this->assertNoErrors($errors); } @@ -1061,6 +1084,7 @@ public function testBug8376(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.0')] public function testAssertDocblock(): void { $errors = $this->runAnalyse(__DIR__ . '/nsrt/assert-docblock.php'); @@ -1075,16 +1099,20 @@ public function testAssertDocblock(): void $this->assertSame(238, $errors[3]->getLine()); } + #[RequiresPhp('>= 8.0')] public function testBug8147(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-8147.php'); $this->assertNoErrors($errors); } + #[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'); @@ -1109,12 +1137,9 @@ public function testBug8503(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.0')] public function testBug8537(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-8537.php'); $this->assertNoErrors($errors); } @@ -1162,12 +1187,9 @@ public function testSkipCheckNoGenericClasses(): void $this->assertSame('Method SkipCheckNoGenericClasses\Foo::doFoo() has parameter $i with generic class LimitIterator but does not specify its types: TKey, TValue, TIterator', $errors[0]->getMessage()); } + #[RequiresPhp('>= 8.1')] public function testBug8983(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-8983.php'); $this->assertNoErrors($errors); } @@ -1188,8 +1210,7 @@ public function testBug9459(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-9459.php'); $this->assertCount(1, $errors); - $this->assertSame('PHPDoc tag @var with type callable(): array is not subtype of native type Closure(): array{}.', $errors[0]->getMessage()); - $this->assertSame(10, $errors[0]->getLine()); + $this->assertSame('PHPDoc tag @var with type callable(): array is not subtype of native type Closure(): array{}.', $errors[0]->getMessage()); } public function testBug9573(): void @@ -1205,12 +1226,9 @@ public function testBug9039(): void $this->assertSame('Constant Bug9039\Test::RULES is unused.', $errors[0]->getMessage()); } + #[RequiresPhp('>= 8.0')] public function testDiscussion9053(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/discussion-9053.php'); $this->assertNoErrors($errors); } @@ -1221,12 +1239,9 @@ public function testProcessCalledMethodInfiniteLoop(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.0')] public function testBug9428(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-9428.php'); $this->assertNoErrors($errors); } @@ -1258,34 +1273,25 @@ public function testIgnoreIdentifiers(): void $this->assertSame(16, $errors[4]->getLine()); } + #[RequiresPhp('>= 8.1')] public function testBug9994(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-9994.php'); $this->assertCount(2, $errors); $this->assertSame('Negated boolean expression is always false.', $errors[0]->getMessage()); $this->assertSame('Parameter #2 $callback of function array_filter expects (callable(1|2|3|null): bool)|null, false given.', $errors[1]->getMessage()); } + #[RequiresPhp('>= 8.1')] public function testBug10049(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-10049-recursive.php'); $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.0')] public function testBug10086(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-10086.php'); $this->assertNoErrors($errors); } @@ -1296,12 +1302,9 @@ public function testBug10147(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.2')] public function testBug10302(): void { - if (PHP_VERSION_ID < 80200) { - $this->markTestSkipped('Test requires PHP 8.2'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-10302.php'); $this->assertNoErrors($errors); } @@ -1328,41 +1331,36 @@ public function testBug10538(): void $this->assertNoErrors($errors); } - public function testBug10772(): void + #[RequiresPhp('>= 8.1')] + public function testBug10847(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10847.php'); + $this->assertNoErrors($errors); + } + #[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 { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-10985.php'); $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.1')] public function testBug10979(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-10979.php'); $this->assertNoErrors($errors); } public function testBug11026(): void { - if (PHP_VERSION_ID < 70300) { - $this->markTestSkipped('Test requires PHP 7.3.'); - } $errors = $this->runAnalyse(__DIR__ . '/data/bug-11026.php'); $this->assertNoErrors($errors); } @@ -1373,27 +1371,22 @@ public function testBug10867(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.1')] public function testBug11263(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-11263.php'); $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.0')] public function testBug11147(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-11147.php'); $this->assertCount(1, $errors); $this->assertSame('Method Bug11147\RedisAdapter::createConnection() has invalid return type Bug11147\NonExistentClass.', $errors[0]->getMessage()); } + #[RequiresPhp('>= 8.0')] public function testBug11283(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-11283.php'); @@ -1406,37 +1399,123 @@ public function testBug11292(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.1')] public function testBug11297(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-11297.php'); $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.0')] public function testBug5597(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-5597.php'); $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.0')] public function testBug11511(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-11511.php'); $this->assertCount(1, $errors); $this->assertSame('Access to an undefined property object::$bar.', $errors[0]->getMessage()); } + public function 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[] diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index b149c39ab0..297d612c8d 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser; +use Nette\DI\Container; use PhpParser\Lexer; use PhpParser\NodeVisitor\NameResolver; use PhpParser\Parser\Php7; @@ -10,6 +11,7 @@ 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\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; @@ -19,6 +21,8 @@ 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; @@ -26,6 +30,7 @@ use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\FileTypeMapper; +use PHPUnit\Framework\Attributes\DataProvider; use stdClass; use function array_map; use function array_merge; @@ -111,6 +116,28 @@ public function testFileWithAnIgnoredErrorMessages(): void $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 testIgnoringBrokenConfigurationDoesNotWork(): void { $this->markTestIncomplete(); @@ -148,7 +175,7 @@ public function testIgnoreErrorMultiByPath(): void $this->assertNoErrors($result); } - public function dataIgnoreErrorByPathAndCount(): iterable + public static function dataIgnoreErrorByPathAndCount(): iterable { yield [ [ @@ -191,16 +218,16 @@ public function dataIgnoreErrorByPathAndCount(): iterable } /** - * @dataProvider dataIgnoreErrorByPathAndCount * @param mixed[] $ignoreErrors */ + #[DataProvider('dataIgnoreErrorByPathAndCount')] public function testIgnoreErrorByPathAndCount(array $ignoreErrors): void { $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/two-fails.php', false); $this->assertNoErrors($result); } - public function dataTrueAndFalse(): array + public static function dataTrueAndFalse(): array { return [ [true], @@ -208,9 +235,7 @@ public function dataTrueAndFalse(): array ]; } - /** - * @dataProvider dataTrueAndFalse - */ + #[DataProvider('dataTrueAndFalse')] public function testIgnoreErrorByPathAndIdentifierCountsCorrectly(bool $onlyFiles): void { $ignoreErrors = [ @@ -234,9 +259,7 @@ public function testIgnoreErrorByPathAndIdentifierCountsCorrectly(bool $onlyFile $this->assertNoErrors($result); } - /** - * @dataProvider dataTrueAndFalse - */ + #[DataProvider('dataTrueAndFalse')] public function testIgnoreErrorByPathAndCountMoreThanExpected(bool $onlyFiles): void { $ignoreErrors = [ @@ -265,9 +288,7 @@ public function testIgnoreErrorByPathAndCountMoreThanExpected(bool $onlyFiles): $this->assertSamePaths(__DIR__ . '/data/two-fails.php', $result[2]->getFile()); } - /** - * @dataProvider dataTrueAndFalse - */ + #[DataProvider('dataTrueAndFalse')] public function testIgnoreErrorByPathAndCountLessThanExpected(bool $onlyFiles): void { $ignoreErrors = [ @@ -418,7 +439,7 @@ public function testIgnoreErrorNotFoundInPathExplicitReportUnmatched(): 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 static function dataIgnoreErrorInTraitUsingClassFilePath(): array { return [ [ @@ -430,9 +451,7 @@ public function dataIgnoreErrorInTraitUsingClassFilePath(): array ]; } - /** - * @dataProvider dataIgnoreErrorInTraitUsingClassFilePath - */ + #[DataProvider('dataIgnoreErrorInTraitUsingClassFilePath')] public function testIgnoreErrorInTraitUsingClassFilePath(string $pathToIgnore): void { $ignoreErrors = [ @@ -483,9 +502,7 @@ public function testReportMultipleParserErrorsAtOnce(): void $this->assertSame(10, $errorTwo->getLine()); } - /** - * @dataProvider dataTrueAndFalse - */ + #[DataProvider('dataTrueAndFalse')] public function testDoNotReportUnmatchedIgnoredErrorsFromPathIfPathWasNotAnalysed(bool $onlyFiles): void { $ignoreErrors = [ @@ -504,9 +521,7 @@ public function testDoNotReportUnmatchedIgnoredErrorsFromPathIfPathWasNotAnalyse $this->assertNoErrors($result); } - /** - * @dataProvider dataTrueAndFalse - */ + #[DataProvider('dataTrueAndFalse')] public function testDoNotReportUnmatchedIgnoredErrorsFromPathWithCountIfPathWasNotAnalysed(bool $onlyFiles): void { $ignoreErrors = [ @@ -555,21 +570,7 @@ public function testIgnoreNextLineUnmatched(): void } } - public function testIgnoreNextLineLegacyBehaviour(): void - { - $result = $this->runAnalyser([], false, [__DIR__ . '/data/ignore-next-line-legacy.php'], true, false); - - foreach ([10, 32, 36] as $i => $line) { - $this->assertArrayHasKey($i, $result); - $this->assertInstanceOf(Error::class, $result[$i]); - $this->assertSame('Fail.', $result[$i]->getMessage()); - $this->assertSame($line, $result[$i]->getLine()); - } - } - - /** - * @dataProvider dataTrueAndFalse - */ + #[DataProvider('dataTrueAndFalse')] public function testIgnoreLine(bool $reportUnmatchedIgnoredErrors): void { $result = $this->runAnalyser([], $reportUnmatchedIgnoredErrors, [ @@ -653,10 +654,9 @@ private function runAnalyser( bool $reportUnmatchedIgnoredErrors, $filePaths, bool $onlyFiles, - bool $enableIgnoreErrorsWithinPhpDocs = true, ): array { - $analyser = $this->createAnalyser($enableIgnoreErrorsWithinPhpDocs); + $analyser = $this->createAnalyser(); if (is_string($filePaths)) { $filePaths = [$filePaths]; @@ -678,9 +678,10 @@ private function runAnalyser( $finalizer = new AnalyserResultFinalizer( new DirectRuleRegistry([]), - new RuleErrorTransformer(), + new IgnoreErrorExtensionProvider(new NetteContainer(new Container([]))), + self::getContainer()->getByType(RuleErrorTransformer::class), $this->createScopeFactory( - $this->createReflectionProvider(), + self::createReflectionProvider(), self::getContainer()->getService('typeSpecifier'), ), new LocalIgnoresProcessor(), @@ -701,14 +702,14 @@ private function runAnalyser( ); } - private function createAnalyser(bool $enableIgnoreErrorsWithinPhpDocs): Analyser + private function createAnalyser(): Analyser { $ruleRegistry = new DirectRuleRegistry([ new AlwaysFailRule(), ]); $collectorRegistry = new CollectorRegistry([]); - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $fileHelper = $this->getFileHelper(); $typeSpecifier = self::getContainer()->getService('typeSpecifier'); @@ -726,6 +727,8 @@ private function createAnalyser(bool $enableIgnoreErrorsWithinPhpDocs): Analyser 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, @@ -735,29 +738,27 @@ private function createAnalyser(bool $enableIgnoreErrorsWithinPhpDocs): Analyser self::createScopeFactory($reflectionProvider, $typeSpecifier), false, true, + true, [], [], [stdClass::class], true, $this->shouldTreatPhpDocTypesAsCertain(), - self::getContainer()->getParameter('featureToggles')['detectDeadTypeInMultiCatch'], - self::getContainer()->getParameter('featureToggles')['paramOutType'], - self::getContainer()->getParameter('featureToggles')['preciseMissingReturn'], + true, ); - $lexer = new Lexer(['usedAttributes' => ['comments', 'startLine', 'endLine', 'startTokenPos', 'endTokenPos']]); + $lexer = new Lexer(); $fileAnalyser = new FileAnalyser( $this->createScopeFactory($reflectionProvider, $typeSpecifier), $nodeScopeResolver, new RichParser( new Php7($lexer), - $lexer, new NameResolver(), self::getContainer(), new IgnoreLexer(), - $enableIgnoreErrorsWithinPhpDocs, ), - new DependencyResolver($fileHelper, $reflectionProvider, new ExportedNodeResolver($fileTypeMapper, new ExprPrinter(new Printer())), $fileTypeMapper), - new RuleErrorTransformer(), + 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(), ); diff --git a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php index 023d00e9f4..914064439f 100644 --- a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php @@ -2,20 +2,22 @@ namespace PHPStan\Analyser; +use Override; use PHPStan\File\FileHelper; 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; -use const PHP_VERSION_ID; class AnalyserTraitsIntegrationTest extends PHPStanTestCase { private FileHelper $fileHelper; + #[Override] protected function setUp(): void { $this->fileHelper = self::getContainer()->getByType(FileHelper::class); @@ -169,12 +171,9 @@ public function testMissingReturnInAbstractTraitMethod(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.1')] public function testUnititializedReadonlyPropertyAccessedInTrait(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped(); - } - $errors = $this->runAnalyse([ __DIR__ . '/traits/uninitializedProperty/FooClass.php', __DIR__ . '/traits/uninitializedProperty/FooTrait.php', 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 index 8d4f675a66..9b92249486 100644 --- a/tests/PHPStan/Analyser/ArgumentsNormalizerLegacyTest.php +++ b/tests/PHPStan/Analyser/ArgumentsNormalizerLegacyTest.php @@ -9,12 +9,11 @@ use PhpParser\Node\Scalar\LNumber; use PhpParser\Node\Scalar\String_; use PHPStan\Node\Expr\TypeExpr; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\SignatureMap\NativeFunctionReflectionProvider; use PHPStan\ShouldNotHappenException; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Constant\ConstantIntegerType; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; final class ArgumentsNormalizerLegacyTest extends PHPStanTestCase { @@ -22,34 +21,25 @@ final class ArgumentsNormalizerLegacyTest extends PHPStanTestCase /** * function call, all arguments named and given in order */ + #[RequiresPhp('>= 8.0')] public function testArgumentReorderAllNamed(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $funcName = new Name('json_encode'); $reflectionProvider = self::getContainer()->getByType(NativeFunctionReflectionProvider::class); $functionReflection = $reflectionProvider->findFunctionReflection('json_encode'); if ($functionReflection === null) { throw new ShouldNotHappenException(); } - $parameterAcceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()); + $parameterAcceptor = $functionReflection->getOnlyVariant(); $args = [ new Arg( new LNumber(0), - false, - false, - [], - new Identifier('flags'), + name: new Identifier('flags'), ), new Arg( new String_('my json value'), - false, - false, - [], - new Identifier('value'), + name: new Identifier('value'), ), ]; $funcCall = new FuncCall($funcName, $args); @@ -72,34 +62,25 @@ public function testArgumentReorderAllNamed(): void /** * function call, all args named, not in order */ + #[RequiresPhp('>= 8.0')] public function testArgumentReorderAllNamedWithSkipped(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $funcName = new Name('json_encode'); $reflectionProvider = self::getContainer()->getByType(NativeFunctionReflectionProvider::class); $functionReflection = $reflectionProvider->findFunctionReflection('json_encode'); if ($functionReflection === null) { throw new ShouldNotHappenException(); } - $parameterAcceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()); + $parameterAcceptor = $functionReflection->getOnlyVariant(); $args = [ new Arg( new LNumber(128), - false, - false, - [], - new Identifier('depth'), + name: new Identifier('depth'), ), new Arg( new String_('my json value'), - false, - false, - [], - new Identifier('value'), + name: new Identifier('value'), ), ]; $funcCall = new FuncCall($funcName, $args); @@ -125,27 +106,21 @@ public function testArgumentReorderAllNamedWithSkipped(): void $this->assertSame(128, $reorderedArgs[2]->value->value); } + #[RequiresPhp('>= 8.0')] public function testMissingRequiredParameter(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $funcName = new Name('json_encode'); $reflectionProvider = self::getContainer()->getByType(NativeFunctionReflectionProvider::class); $functionReflection = $reflectionProvider->findFunctionReflection('json_encode'); if ($functionReflection === null) { throw new ShouldNotHappenException(); } - $parameterAcceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()); + $parameterAcceptor = $functionReflection->getOnlyVariant(); $args = [ new Arg( new LNumber(128), - false, - false, - [], - new Identifier('depth'), + name: new Identifier('depth'), ), ]; $funcCall = new FuncCall($funcName, $args); @@ -161,7 +136,7 @@ public function testLeaveRegularCallAsIs(): void if ($functionReflection === null) { throw new ShouldNotHappenException(); } - $parameterAcceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()); + $parameterAcceptor = $functionReflection->getOnlyVariant(); $args = [ new Arg( diff --git a/tests/PHPStan/Analyser/ArgumentsNormalizerTest.php b/tests/PHPStan/Analyser/ArgumentsNormalizerTest.php index 3d28594e7e..b2cc10be5d 100644 --- a/tests/PHPStan/Analyser/ArgumentsNormalizerTest.php +++ b/tests/PHPStan/Analyser/ArgumentsNormalizerTest.php @@ -17,12 +17,13 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; use function count; class ArgumentsNormalizerTest extends PHPStanTestCase { - public function dataReorderValid(): iterable + public static function dataReorderValid(): iterable { yield [ [ @@ -245,11 +246,11 @@ public function dataReorderValid(): iterable } /** - * @dataProvider dataReorderValid - * @param array $parameterSettings - * @param array $argumentSettings + * @param array $parameterSettings + * @param array $argumentSettings * @param array $expectedArgumentTypes */ + #[DataProvider('dataReorderValid')] public function testReorderValid( array $parameterSettings, array $argumentSettings, @@ -270,7 +271,7 @@ public function testReorderValid( $arguments = []; foreach ($argumentSettings as [$type, $name]) { - $arguments[] = new Arg(new TypeExpr($type), false, false, [], $name === null ? null : new Identifier($name)); + $arguments[] = new Arg(new TypeExpr($type), name: $name === null ? null : new Identifier($name)); } $normalized = ArgumentsNormalizer::reorderFuncArguments( @@ -298,7 +299,7 @@ public function testReorderValid( } } - public function dataReorderInvalid(): iterable + public static function dataReorderInvalid(): iterable { yield [ [ @@ -325,10 +326,10 @@ public function dataReorderInvalid(): iterable } /** - * @dataProvider dataReorderInvalid - * @param array $parameterSettings - * @param array $argumentSettings + * @param array $parameterSettings + * @param array $argumentSettings */ + #[DataProvider('dataReorderInvalid')] public function testReorderInvalid( array $parameterSettings, array $argumentSettings, @@ -348,7 +349,7 @@ public function testReorderInvalid( $arguments = []; foreach ($argumentSettings as [$type, $name]) { - $arguments[] = new Arg(new TypeExpr($type), false, false, [], $name === null ? null : new Identifier($name)); + $arguments[] = new Arg(new TypeExpr($type), name: $name === null ? null : new Identifier($name)); } $normalized = ArgumentsNormalizer::reorderFuncArguments( diff --git a/tests/PHPStan/Analyser/AssertStubTest.php b/tests/PHPStan/Analyser/AssertStubTest.php index 2ea24ac4ce..579e52e359 100644 --- a/tests/PHPStan/Analyser/AssertStubTest.php +++ b/tests/PHPStan/Analyser/AssertStubTest.php @@ -3,19 +3,20 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class AssertStubTest extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-stub.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/assert-stub.php'); } /** - * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/Bug10922Test.php b/tests/PHPStan/Analyser/Bug10922Test.php index 2dccf946c8..6fa62ea5e7 100644 --- a/tests/PHPStan/Analyser/Bug10922Test.php +++ b/tests/PHPStan/Analyser/Bug10922Test.php @@ -3,19 +3,20 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class Bug10922Test extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10922.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/bug-10922.php'); } /** - * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/Bug10980Test.php b/tests/PHPStan/Analyser/Bug10980Test.php index 6c3821447f..51422dcd45 100644 --- a/tests/PHPStan/Analyser/Bug10980Test.php +++ b/tests/PHPStan/Analyser/Bug10980Test.php @@ -3,19 +3,20 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class Bug10980Test extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { yield from self::gatherAssertTypes(__DIR__ . '/data/bug-10980.php'); } /** - * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/Bug11009Test.php b/tests/PHPStan/Analyser/Bug11009Test.php index 480e170756..d558eb0a0f 100644 --- a/tests/PHPStan/Analyser/Bug11009Test.php +++ b/tests/PHPStan/Analyser/Bug11009Test.php @@ -3,19 +3,20 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class Bug11009Test extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-11009.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/bug-11009.php'); } /** - * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php index ff0a0daa9e..a7342e7506 100644 --- a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php +++ b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Analyser; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\Methods\CallMethodsRule; use PHPStan\Rules\Methods\MethodCallCheck; @@ -12,7 +11,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -22,11 +20,11 @@ class Bug9307CallMethodsRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, true, false, true, false); + $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 PhpVersion(PHP_VERSION_ID), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), + new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), ); } diff --git a/tests/PHPStan/Analyser/Bug9307Test.php b/tests/PHPStan/Analyser/Bug9307Test.php index 6502ac2701..1de98619ea 100644 --- a/tests/PHPStan/Analyser/Bug9307Test.php +++ b/tests/PHPStan/Analyser/Bug9307Test.php @@ -3,19 +3,20 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class Bug9307Test extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9307.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/bug-9307.php'); } /** - * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/ClassConstantStubFileTest.php b/tests/PHPStan/Analyser/ClassConstantStubFileTest.php index 00d83693e6..2d1cb36f58 100644 --- a/tests/PHPStan/Analyser/ClassConstantStubFileTest.php +++ b/tests/PHPStan/Analyser/ClassConstantStubFileTest.php @@ -3,19 +3,20 @@ 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 mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/ConditionalReturnTypeFromMethodStubTest.php b/tests/PHPStan/Analyser/ConditionalReturnTypeFromMethodStubTest.php index 84be755a38..af19568166 100644 --- a/tests/PHPStan/Analyser/ConditionalReturnTypeFromMethodStubTest.php +++ b/tests/PHPStan/Analyser/ConditionalReturnTypeFromMethodStubTest.php @@ -3,19 +3,20 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class ConditionalReturnTypeFromMethodStubTest extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/conditional-return-type-stub.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/conditional-return-type-stub.php'); } /** - * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, 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 index 61c93088f9..75a9797ee3 100644 --- a/tests/PHPStan/Analyser/DoNotRememberPossiblyImpureFunctionValuesTest.php +++ b/tests/PHPStan/Analyser/DoNotRememberPossiblyImpureFunctionValuesTest.php @@ -3,19 +3,20 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class DoNotRememberPossiblyImpureFunctionValuesTest extends TypeInferenceTestCase { - public function dataAsserts(): iterable + public static function dataAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/do-not-remember-possibly-impure-function-values.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/do-not-remember-possibly-impure-function-values.php'); } /** - * @dataProvider dataAsserts * @param mixed ...$args */ + #[DataProvider('dataAsserts')] public function testAsserts( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php b/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php index 0d5f8a8dc8..9a55ab9360 100644 --- a/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php +++ b/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php @@ -3,25 +3,26 @@ 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 { if (PHP_VERSION_ID < 80000) { return []; } - yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-throw-type-extension.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-throw-type-extension-named-args-fixture.php'); + 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 mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php b/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php index 59ebef356e..d336534264 100644 --- a/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php +++ b/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php @@ -3,29 +3,29 @@ 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 self::gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-types.php'); if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-types-named-args.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-types-named-args.php'); } - yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-compound-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-getsingle-conditional.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7344.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7391b.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 mixed ...$args */ + #[DataProvider('dataAsserts')] public function testAsserts( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/ErrorTest.php b/tests/PHPStan/Analyser/ErrorTest.php index 7bd49a242a..e59abafff3 100644 --- a/tests/PHPStan/Analyser/ErrorTest.php +++ b/tests/PHPStan/Analyser/ErrorTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser; use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class ErrorTest extends PHPStanTestCase { @@ -15,7 +16,7 @@ public function testError(): void $this->assertSame(10, $error->getLine()); } - public function dataValidIdentifier(): iterable + public static function dataValidIdentifier(): iterable { yield ['a']; yield ['aa']; @@ -29,15 +30,13 @@ public function dataValidIdentifier(): iterable yield ['3m.blah']; } - /** - * @dataProvider dataValidIdentifier - */ + #[DataProvider('dataValidIdentifier')] public function testValidIdentifier(string $identifier): void { $this->assertTrue(Error::validateIdentifier($identifier)); } - public function dataInvalidIdentifier(): iterable + public static function dataInvalidIdentifier(): iterable { yield ['']; yield [' ']; @@ -48,9 +47,7 @@ public function dataInvalidIdentifier(): iterable yield ['.']; } - /** - * @dataProvider dataInvalidIdentifier - */ + #[DataProvider('dataInvalidIdentifier')] public function testInvalidIdentifier(string $identifier): void { $this->assertFalse(Error::validateIdentifier($identifier)); 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 index b8dda807d4..6c3d707942 100644 --- a/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php +++ b/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php @@ -3,19 +3,20 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class ExpressionTypeResolverExtensionTest extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { yield from self::gatherAssertTypes(__DIR__ . '/data/expression-type-resolver-extension.php'); } /** - * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/Ignore/IgnoreLexerTest.php b/tests/PHPStan/Analyser/Ignore/IgnoreLexerTest.php index 168b6799b1..6e46630df6 100644 --- a/tests/PHPStan/Analyser/Ignore/IgnoreLexerTest.php +++ b/tests/PHPStan/Analyser/Ignore/IgnoreLexerTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser\Ignore; use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; use function array_pop; use function substr_count; use const PHP_EOL; @@ -10,7 +11,7 @@ class IgnoreLexerTest extends PHPStanTestCase { - public function dataTokenize(): iterable + public static function dataTokenize(): iterable { yield [ '', @@ -80,9 +81,9 @@ public function dataTokenize(): iterable } /** - * @dataProvider dataTokenize * @param list $expectedTokens */ + #[DataProvider('dataTokenize')] public function testTokenize(string $input, array $expectedTokens): void { $lexer = new IgnoreLexer(); diff --git a/tests/PHPStan/Analyser/ImmediatelyCalledFunctionWithoutImplicitThrowTest.php b/tests/PHPStan/Analyser/ImmediatelyCalledFunctionWithoutImplicitThrowTest.php index f810ed04a2..8a9e2ee7a6 100644 --- a/tests/PHPStan/Analyser/ImmediatelyCalledFunctionWithoutImplicitThrowTest.php +++ b/tests/PHPStan/Analyser/ImmediatelyCalledFunctionWithoutImplicitThrowTest.php @@ -3,20 +3,21 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; use function array_merge; class ImmediatelyCalledFunctionWithoutImplicitThrowTest extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/immediately-called-function-without-implicit-throw.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/immediately-called-function-without-implicit-throw.php'); } /** - * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, 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 4d0ca33581..24ab77c1a0 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -16,6 +16,8 @@ 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; @@ -63,10 +65,10 @@ public function testClassMethodScope(): void }); } - private function getFileScope(string $filename): Scope + private static function getFileScope(string $filename): Scope { $testScope = null; - $this->processFile($filename, static function (Node $node, Scope $scope) use (&$testScope): void { + self::processFile($filename, static function (Node $node, Scope $scope) use (&$testScope): void { if (!($node instanceof Exit_)) { return; } @@ -78,7 +80,7 @@ private function getFileScope(string $filename): Scope return $testScope; } - public function dataUnionInCatch(): array + public static function dataUnionInCatch(): array { return [ [ @@ -88,9 +90,7 @@ public function dataUnionInCatch(): array ]; } - /** - * @dataProvider dataUnionInCatch - */ + #[DataProvider('dataUnionInCatch')] public function testUnionInCatch( string $description, string $expression, @@ -103,7 +103,7 @@ public function testUnionInCatch( ); } - public function dataUnionAndIntersection(): array + public static function dataUnionAndIntersection(): array { return [ [ @@ -213,9 +213,7 @@ public function dataUnionAndIntersection(): array ]; } - /** - * @dataProvider dataUnionAndIntersection - */ + #[DataProvider('dataUnionAndIntersection')] public function testUnionAndIntersection( string $description, string $expression, @@ -228,9 +226,9 @@ public function testUnionAndIntersection( ); } - public function dataAssignInIf(): array + public static function dataAssignInIf(): array { - $testScope = $this->getFileScope(__DIR__ . '/data/if.php'); + $testScope = self::getFileScope(__DIR__ . '/data/if.php'); return [ [ @@ -355,7 +353,7 @@ public function dataAssignInIf(): array $testScope, 'matches3', TrinaryLogic::createYes(), - 'array{0?: string}', + 'array{}|array{string}', ], [ $testScope, @@ -415,7 +413,7 @@ public function dataAssignInIf(): array $testScope, 'ternaryMatches', TrinaryLogic::createYes(), - 'array{0?: string}', + 'array{string}', ], [ $testScope, @@ -738,9 +736,7 @@ public function dataAssignInIf(): array ]; } - /** - * @dataProvider dataAssignInIf - */ + #[DataProvider('dataAssignInIf')] public function testAssignInIf( Scope $scope, string $variableName, @@ -758,9 +754,9 @@ public function testAssignInIf( ); } - public function dataConstantTypes(): array + public static function dataConstantTypes(): array { - $testScope = $this->getFileScope(__DIR__ . '/data/constantTypes.php'); + $testScope = self::getFileScope(__DIR__ . '/data/constantTypes.php'); return [ [ @@ -876,9 +872,7 @@ public function dataConstantTypes(): array ]; } - /** - * @dataProvider dataConstantTypes - */ + #[DataProvider('dataConstantTypes')] public function testConstantTypes( Scope $scope, string $variableName, @@ -906,7 +900,7 @@ private function assertVariables( $this->assertTrue( $expectedCertainty->equals($certainty), sprintf( - 'Certainty of variable $%s is %s, expected %s', + 'Certainty of %s is %s, expected %s', $variableName, $certainty->describe(), $expectedCertainty->describe(), @@ -941,7 +935,7 @@ private function assertVariables( } } - public function dataArrayDestructuring(): array + public static function dataArrayDestructuring(): array { return [ [ @@ -1129,11 +1123,11 @@ public function dataArrayDestructuring(): array '$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]', ], [ @@ -1211,9 +1205,7 @@ public function dataArrayDestructuring(): array ]; } - /** - * @dataProvider dataArrayDestructuring - */ + #[DataProvider('dataArrayDestructuring')] public function testArrayDestructuring( string $description, string $expression, @@ -1226,7 +1218,7 @@ public function testArrayDestructuring( ); } - public function dataParameterTypes(): array + public static function dataParameterTypes(): array { return [ [ @@ -1292,9 +1284,7 @@ public function dataParameterTypes(): array ]; } - /** - * @dataProvider dataParameterTypes - */ + #[DataProvider('dataParameterTypes')] public function testTypehints( string $typeClass, string $expression, @@ -1307,7 +1297,7 @@ public function testTypehints( ); } - public function dataAnonymousFunctionParameterTypes(): array + public static function dataAnonymousFunctionParameterTypes(): array { return [ [ @@ -1353,9 +1343,7 @@ public function dataAnonymousFunctionParameterTypes(): array ]; } - /** - * @dataProvider dataAnonymousFunctionParameterTypes - */ + #[DataProvider('dataAnonymousFunctionParameterTypes')] public function testAnonymousFunctionTypehints( string $description, string $expression, @@ -1368,7 +1356,7 @@ public function testAnonymousFunctionTypehints( ); } - public function dataVarAnnotations(): array + public static function dataVarAnnotations(): array { return [ [ @@ -1434,9 +1422,7 @@ public function dataVarAnnotations(): array ]; } - /** - * @dataProvider dataVarAnnotations - */ + #[DataProvider('dataVarAnnotations')] public function testVarAnnotations( string $description, string $expression, @@ -1452,7 +1438,7 @@ public function testVarAnnotations( ); } - public function dataCasts(): array + public static function dataCasts(): array { return [ [ @@ -1552,7 +1538,7 @@ public function dataCasts(): array '(float) $str', ], [ - "array{\0TypesNamespaceCasts\\Foo\0foo: TypesNamespaceCasts\\Foo, \0TypesNamespaceCasts\\Foo\0int: int, \0*\0protectedInt: int, publicInt: int, \0TypesNamespaceCasts\\Bar\0barProperty: TypesNamespaceCasts\\Bar}", + "array{'\0TypesNamespaceCasts\\Foo\0foo': TypesNamespaceCasts\\Foo, '\0TypesNamespaceCasts\\Foo\0int': int, '\0*\0protectedInt': int, publicInt: int, '\0TypesNamespaceCasts\\Bar\0barProperty': TypesNamespaceCasts\\Bar}", '(array) $foo', ], [ @@ -1590,9 +1576,7 @@ public function dataCasts(): array ]; } - /** - * @dataProvider dataCasts - */ + #[DataProvider('dataCasts')] public function testCasts( string $desciptiion, string $expression, @@ -1605,7 +1589,7 @@ public function testCasts( ); } - public function dataDeductedTypes(): array + public static function dataDeductedTypes(): array { return [ [ @@ -1719,9 +1703,7 @@ public function dataDeductedTypes(): array ]; } - /** - * @dataProvider dataDeductedTypes - */ + #[DataProvider('dataDeductedTypes')] public function testDeductedTypes( string $description, string $expression, @@ -1735,7 +1717,7 @@ public function testDeductedTypes( ); } - public function dataProperties(): array + public static function dataProperties(): array { return [ [ @@ -1763,7 +1745,7 @@ public function dataProperties(): array '$this->arrayPropertyOne', ], [ - 'array', + 'array', '$this->arrayPropertyOther', ], [ @@ -1865,9 +1847,7 @@ public function dataProperties(): array ]; } - /** - * @dataProvider dataProperties - */ + #[DataProvider('dataProperties')] public function testProperties( string $description, string $expression, @@ -1880,7 +1860,7 @@ public function testProperties( ); } - public function dataBinaryOperations(): array + public static function dataBinaryOperations(): array { $typeCallback = static function ($value): string { if (is_int($value)) { @@ -2363,14 +2343,42 @@ public function dataBinaryOperations(): array 'array', 'max($arrayOfUnknownIntegers, $arrayOfUnknownIntegers)', ], - /*[ - 'array(1, 1, 1, 1)', + [ + 'array{1, 1, 1, 1}', 'max(array(2, 2, 2), 5, array(1, 1, 1, 1))', ], + [ + 'array{int, int, int}', + 'max($arrayOfIntegers, 5)', + ], [ 'array', + 'max($arrayOfUnknownIntegers, 5)', + ], + [ + 'array|int', // could be array 'max($arrayOfUnknownIntegers, $integer, $arrayOfUnknownIntegers)', - ],*/ + ], + [ + 'array', + 'max($arrayOfUnknownIntegers, $conditionalInt)', + ], + [ + '5', + 'min($arrayOfIntegers, 5)', + ], + [ + '5', + 'min($arrayOfUnknownIntegers, 5)', + ], + [ + '1|2', + 'min($arrayOfUnknownIntegers, $conditionalInt)', + ], + [ + '5', + 'min(array(2, 2, 2), 5, array(1, 1, 1, 1))', + ], [ '1.1', 'min(...[1.1, 2.2, 3.3])', @@ -2904,7 +2912,7 @@ public function dataBinaryOperations(): array '$fooString[4]', ], [ - 'non-empty-string', + "''|'f'|'o'", '$fooString[$integer]', ], [ @@ -3136,7 +3144,7 @@ public function dataBinaryOperations(): array '$simpleXMLWritingXML', ], [ - 'array', + 'array|null', '$simpleXMLRightXpath', ], [ @@ -3154,9 +3162,7 @@ public function dataBinaryOperations(): array ]; } - /** - * @dataProvider dataBinaryOperations - */ + #[DataProvider('dataBinaryOperations')] public function testBinaryOperations( string $description, string $expression, @@ -3169,7 +3175,7 @@ public function testBinaryOperations( ); } - public function dataVarStatementAnnotation(): array + public static function dataVarStatementAnnotation(): array { return [ [ @@ -3179,9 +3185,7 @@ public function dataVarStatementAnnotation(): array ]; } - /** - * @dataProvider dataVarStatementAnnotation - */ + #[DataProvider('dataVarStatementAnnotation')] public function testVarStatementAnnotation( string $description, string $expression, @@ -3194,7 +3198,7 @@ public function testVarStatementAnnotation( ); } - public function dataCloneOperators(): array + public static function dataCloneOperators(): array { return [ [ @@ -3204,9 +3208,7 @@ public function dataCloneOperators(): array ]; } - /** - * @dataProvider dataCloneOperators - */ + #[DataProvider('dataCloneOperators')] public function testCloneOperators( string $description, string $expression, @@ -3219,7 +3221,7 @@ public function testCloneOperators( ); } - public function dataLiteralArrays(): array + public static function dataLiteralArrays(): array { return [ [ @@ -3257,9 +3259,7 @@ public function dataLiteralArrays(): array ]; } - /** - * @dataProvider dataLiteralArrays - */ + #[DataProvider('dataLiteralArrays')] public function testLiteralArrays( string $description, string $expression, @@ -3272,7 +3272,7 @@ public function testLiteralArrays( ); } - public function dataLiteralArraysKeys(): array + public static function dataLiteralArraysKeys(): array { define('STRING_ONE', '1'); define('INT_ONE', 1); @@ -3328,15 +3328,13 @@ public function dataLiteralArraysKeys(): array "'BooleansArray'", ], [ - 'int|string', + '(int|string)', "'UnknownConstantArray'", ], ]; } - /** - * @dataProvider dataLiteralArraysKeys - */ + #[DataProvider('dataLiteralArraysKeys')] public function testLiteralArraysKeys( string $description, string $evaluatedPointExpressionType, @@ -3350,7 +3348,7 @@ public function testLiteralArraysKeys( ); } - public function dataStringArrayAccess(): array + public static function dataStringArrayAccess(): array { return [ [ @@ -3376,9 +3374,7 @@ public function dataStringArrayAccess(): array ]; } - /** - * @dataProvider dataStringArrayAccess - */ + #[DataProvider('dataStringArrayAccess')] public function testStringArrayAccess( string $description, string $expression, @@ -3391,7 +3387,7 @@ public function testStringArrayAccess( ); } - public function dataTypeFromFunctionPhpDocs(): array + public static function dataTypeFromFunctionPhpDocs(): array { return [ [ @@ -3423,7 +3419,7 @@ public function dataTypeFromFunctionPhpDocs(): array '$arrayParameterOne', ], [ - 'array', + 'array', '$arrayParameterOther', ], [ @@ -3537,7 +3533,7 @@ public function dataTypeFromFunctionPhpDocs(): array ]; } - public function dataTypeFromFunctionFunctionPhpDocs(): array + public static function dataTypeFromFunctionFunctionPhpDocs(): array { return [ [ @@ -3551,10 +3547,8 @@ public function dataTypeFromFunctionFunctionPhpDocs(): array ]; } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromFunctionFunctionPhpDocs - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromFunctionFunctionPhpDocs')] public function testTypeFromFunctionPhpDocs( string $description, string $expression, @@ -3568,7 +3562,7 @@ public function testTypeFromFunctionPhpDocs( ); } - public function dataTypeFromFunctionPrefixedPhpDocs(): array + public static function dataTypeFromFunctionPrefixedPhpDocs(): array { return [ [ @@ -3578,10 +3572,8 @@ public function dataTypeFromFunctionPrefixedPhpDocs(): array ]; } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromFunctionPrefixedPhpDocs - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromFunctionPrefixedPhpDocs')] public function testTypeFromFunctionPhpDocsPsalmPrefix( string $description, string $expression, @@ -3595,10 +3587,8 @@ public function testTypeFromFunctionPhpDocsPsalmPrefix( ); } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromFunctionPrefixedPhpDocs - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromFunctionPrefixedPhpDocs')] public function testTypeFromFunctionPhpDocsPhpstanPrefix( string $description, string $expression, @@ -3612,10 +3602,8 @@ public function testTypeFromFunctionPhpDocsPhpstanPrefix( ); } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromFunctionPrefixedPhpDocs - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromFunctionPrefixedPhpDocs')] public function testTypeFromFunctionPhpDocsPhanPrefix( string $description, string $expression, @@ -3629,7 +3617,7 @@ public function testTypeFromFunctionPhpDocsPhanPrefix( ); } - public function dataTypeFromMethodPhpDocs(): array + public static function dataTypeFromMethodPhpDocs(): array { return [ [ @@ -3771,10 +3759,8 @@ public function dataTypeFromMethodPhpDocs(): array ]; } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromMethodPhpDocs')] public function testTypeFromMethodPhpDocs( string $description, string $expression, @@ -3787,10 +3773,8 @@ public function testTypeFromMethodPhpDocs( ); } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromMethodPhpDocs')] public function testTypeFromMethodPhpDocsPsalmPrefix( string $description, string $expression, @@ -3813,10 +3797,10 @@ public function testTypeFromMethodPhpDocsPsalmPrefix( } /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs * @param bool $replaceClass = true */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromMethodPhpDocs')] public function testTypeFromMethodPhpDocsPhpstanPrefix( string $description, string $expression, @@ -3838,10 +3822,8 @@ public function testTypeFromMethodPhpDocsPhpstanPrefix( ); } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromMethodPhpDocs')] public function testTypeFromMethodPhpDocsPhanPrefix( string $description, string $expression, @@ -3863,10 +3845,8 @@ public function testTypeFromMethodPhpDocsPhanPrefix( ); } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromMethodPhpDocs')] public function testTypeFromTraitPhpDocs( string $description, string $expression, @@ -3888,10 +3868,8 @@ public function testTypeFromTraitPhpDocs( ); } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromMethodPhpDocs')] public function testTypeFromMethodPhpDocsInheritDocWithoutCurlyBraces( string $description, string $expression, @@ -3913,10 +3891,8 @@ public function testTypeFromMethodPhpDocsInheritDocWithoutCurlyBraces( ); } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromMethodPhpDocs')] public function testTypeFromRecursiveTraitPhpDocs( string $description, string $expression, @@ -3938,7 +3914,7 @@ public function testTypeFromRecursiveTraitPhpDocs( ); } - public function dataTypeFromTraitPhpDocsInSameFile(): array + public static function dataTypeFromTraitPhpDocsInSameFile(): array { return [ [ @@ -3948,9 +3924,7 @@ public function dataTypeFromTraitPhpDocsInSameFile(): array ]; } - /** - * @dataProvider dataTypeFromTraitPhpDocsInSameFile - */ + #[DataProvider('dataTypeFromTraitPhpDocsInSameFile')] public function testTypeFromTraitPhpDocsInSameFile( string $description, string $expression, @@ -3963,10 +3937,8 @@ public function testTypeFromTraitPhpDocsInSameFile( ); } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromMethodPhpDocs')] public function testTypeFromMethodPhpDocsInheritDoc( string $description, string $expression, @@ -3988,10 +3960,8 @@ public function testTypeFromMethodPhpDocsInheritDoc( ); } - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - */ + #[DataProvider('dataTypeFromFunctionPhpDocs')] + #[DataProvider('dataTypeFromMethodPhpDocs')] public function testTypeFromMethodPhpDocsImplicitInheritance( string $description, string $expression, @@ -4022,7 +3992,7 @@ public function testNotSwitchInstanceof(): void ); } - public function dataSwitchInstanceOf(): array + public static function dataSwitchInstanceOf(): array { return [ [ @@ -4040,9 +4010,7 @@ public function dataSwitchInstanceOf(): array ]; } - /** - * @dataProvider dataSwitchInstanceOf - */ + #[DataProvider('dataSwitchInstanceOf')] public function testSwitchInstanceof( string $description, string $expression, @@ -4055,9 +4023,7 @@ public function testSwitchInstanceof( ); } - /** - * @dataProvider dataSwitchInstanceOf - */ + #[DataProvider('dataSwitchInstanceOf')] public function testSwitchInstanceofTruthy( string $description, string $expression, @@ -4070,7 +4036,7 @@ public function testSwitchInstanceofTruthy( ); } - public function dataSwitchGetClass(): array + public static function dataSwitchGetClass(): array { return [ [ @@ -4086,9 +4052,7 @@ public function dataSwitchGetClass(): array ]; } - /** - * @dataProvider dataSwitchGetClass - */ + #[DataProvider('dataSwitchGetClass')] public function testSwitchGetClass( string $description, string $expression, @@ -4103,7 +4067,7 @@ public function testSwitchGetClass( ); } - public function dataSwitchInstanceOfFallthrough(): array + public static function dataSwitchInstanceOfFallthrough(): array { return [ [ @@ -4113,9 +4077,7 @@ public function dataSwitchInstanceOfFallthrough(): array ]; } - /** - * @dataProvider dataSwitchInstanceOfFallthrough - */ + #[DataProvider('dataSwitchInstanceOfFallthrough')] public function testSwitchInstanceOfFallthrough( string $description, string $expression, @@ -4128,7 +4090,7 @@ public function testSwitchInstanceOfFallthrough( ); } - public function dataSwitchTypeElimination(): array + public static function dataSwitchTypeElimination(): array { return [ [ @@ -4138,9 +4100,7 @@ public function dataSwitchTypeElimination(): array ]; } - /** - * @dataProvider dataSwitchTypeElimination - */ + #[DataProvider('dataSwitchTypeElimination')] public function testSwitchTypeElimination( string $description, string $expression, @@ -4153,7 +4113,7 @@ public function testSwitchTypeElimination( ); } - public function dataOverwritingVariable(): array + public static function dataOverwritingVariable(): array { return [ [ @@ -4174,9 +4134,7 @@ public function dataOverwritingVariable(): array ]; } - /** - * @dataProvider dataOverwritingVariable - */ + #[DataProvider('dataOverwritingVariable')] public function testOverwritingVariable( string $description, string $expression, @@ -4191,7 +4149,7 @@ public function testOverwritingVariable( ); } - public function dataNegatedInstanceof(): array + public static function dataNegatedInstanceof(): array { return [ [ @@ -4241,9 +4199,7 @@ public function dataNegatedInstanceof(): array ]; } - /** - * @dataProvider dataNegatedInstanceof - */ + #[DataProvider('dataNegatedInstanceof')] public function testNegatedInstanceof( string $description, string $expression, @@ -4256,7 +4212,7 @@ public function testNegatedInstanceof( ); } - public function dataAnonymousFunction(): array + public static function dataAnonymousFunction(): array { return [ [ @@ -4264,7 +4220,7 @@ public function dataAnonymousFunction(): array '$str', ], [ - PHP_VERSION_ID < 80000 ? 'list' : 'array', + PHP_VERSION_ID < 80000 ? 'list' : 'array', '$arr', ], [ @@ -4278,9 +4234,7 @@ public function dataAnonymousFunction(): array ]; } - /** - * @dataProvider dataAnonymousFunction - */ + #[DataProvider('dataAnonymousFunction')] public function testAnonymousFunction( string $description, string $expression, @@ -4293,7 +4247,7 @@ public function testAnonymousFunction( ); } - public function dataForeachArrayType(): array + public static function dataForeachArrayType(): array { return [ [ @@ -4414,9 +4368,7 @@ public function dataForeachArrayType(): array ]; } - /** - * @dataProvider dataForeachArrayType - */ + #[DataProvider('dataForeachArrayType')] public function testForeachArrayType( string $file, string $description, @@ -4430,7 +4382,7 @@ public function testForeachArrayType( ); } - public function dataOverridingSpecifiedType(): array + public static function dataOverridingSpecifiedType(): array { return [ [ @@ -4441,9 +4393,7 @@ public function dataOverridingSpecifiedType(): array ]; } - /** - * @dataProvider dataOverridingSpecifiedType - */ + #[DataProvider('dataOverridingSpecifiedType')] public function testOverridingSpecifiedType( string $file, string $description, @@ -4457,7 +4407,7 @@ public function testOverridingSpecifiedType( ); } - public function dataForeachObjectType(): array + public static function dataForeachObjectType(): array { return [ [ @@ -4499,9 +4449,7 @@ public function dataForeachObjectType(): array ]; } - /** - * @dataProvider dataForeachObjectType - */ + #[DataProvider('dataForeachObjectType')] public function testForeachObjectType( string $file, string $description, @@ -4517,7 +4465,7 @@ public function testForeachObjectType( ); } - public function dataArrayFunctions(): array + public static function dataArrayFunctions(): array { return [ [ @@ -4577,7 +4525,7 @@ public function dataArrayFunctions(): array '$reducedToInt', ], [ - 'array<0|1|2, 1|2|3>', + 'array{1, 2, 3}', 'array_change_key_case($integers)', ], [ @@ -4846,7 +4794,7 @@ public function dataArrayFunctions(): array 'array_pop($stringKeys)', ], [ - 'array&hasOffsetValue(\'baz\', stdClass)', + 'non-empty-array&hasOffsetValue(\'baz\', stdClass)', '$stdClassesWithIsset', ], [ @@ -4906,7 +4854,7 @@ public function dataArrayFunctions(): array 'array_filter($withPossiblyFalsey)', ], [ - '(array|null)', + PHP_VERSION_ID < 80000 ? '(array|null)' : 'array', 'array_filter($mixed)', ], [ @@ -5228,9 +5176,7 @@ public function dataArrayFunctions(): array ]; } - /** - * @dataProvider dataArrayFunctions - */ + #[DataProvider('dataArrayFunctions')] public function testArrayFunctions( string $description, string $expression, @@ -5243,16 +5189,8 @@ public function testArrayFunctions( ); } - public function dataFunctions(): array + public static function dataFunctions(): array { - $strSplitDefaultReturnType = 'non-empty-list|false'; - if (PHP_VERSION_ID >= 80000) { - $strSplitDefaultReturnType = 'non-empty-list'; - } - if (PHP_VERSION_ID >= 80200) { - $strSplitDefaultReturnType = 'list'; - } - return [ [ 'string', @@ -5299,11 +5237,11 @@ public function dataFunctions(): array '$versionCompare6', ], [ - 'bool', + PHP_VERSION_ID < 80000 ? '(bool|null)' : 'bool', '$versionCompare7', ], [ - 'bool', + PHP_VERSION_ID < 80000 ? '(bool|null)' : 'bool', '$versionCompare8', ], [ @@ -5442,42 +5380,6 @@ public function dataFunctions(): array '(array{sec: int, usec: int, minuteswest: int, dsttime: int}|float)', '$gettimeofdayBenevolent', ], - [ - $strSplitDefaultReturnType, - '$strSplitConstantStringWithoutDefinedParameters', - ], - [ - 'array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', - '$strSplitConstantStringWithoutDefinedSplitLength', - ], - [ - PHP_VERSION_ID < 80200 ? 'non-empty-list' : 'list', - '$strSplitStringWithoutDefinedSplitLength', - ], - [ - 'array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', - '$strSplitConstantStringWithOneSplitLength', - ], - [ - 'array{\'abcdef\'}', - '$strSplitConstantStringWithGreaterSplitLengthThanStringLength', - ], - [ - 'false', - '$strSplitConstantStringWithFailureSplitLength', - ], - [ - $strSplitDefaultReturnType, - '$strSplitConstantStringWithInvalidSplitLengthType', - ], - [ - "array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", - '$strSplitConstantStringWithVariableStringAndConstantSplitLength', - ], - [ - $strSplitDefaultReturnType, - '$strSplitConstantStringWithVariableStringAndVariableSplitLength', - ], // parse_url [ 'array|int|string|false|null', @@ -5492,7 +5394,7 @@ public function dataFunctions(): array '$parseUrlConstantUrlWithoutComponent2', ], [ - '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', + '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', ], [ @@ -5606,9 +5508,7 @@ public function dataFunctions(): array ]; } - /** - * @dataProvider dataFunctions - */ + #[DataProvider('dataFunctions')] public function testFunctions( string $description, string $expression, @@ -5621,7 +5521,7 @@ public function testFunctions( ); } - public function dataDioFunctions(): array + public static function dataDioFunctions(): array { return [ [ @@ -5631,9 +5531,7 @@ public function dataDioFunctions(): array ]; } - /** - * @dataProvider dataDioFunctions - */ + #[DataProvider('dataDioFunctions')] public function testDioFunctions( string $description, string $expression, @@ -5649,7 +5547,7 @@ public function testDioFunctions( ); } - public function dataSsh2Functions(): array + public static function dataSsh2Functions(): array { return [ [ @@ -5659,9 +5557,7 @@ public function dataSsh2Functions(): array ]; } - /** - * @dataProvider dataSsh2Functions - */ + #[DataProvider('dataSsh2Functions')] public function testSsh2Functions( string $description, string $expression, @@ -5674,7 +5570,7 @@ public function testSsh2Functions( ); } - public function dataRangeFunction(): array + public static function dataRangeFunction(): array { return [ [ @@ -5685,6 +5581,10 @@ public function dataRangeFunction(): array 'array{2, 4}', 'range(2, 5, 2)', ], + [ + 'array{2, 0}', + "range(2, '', 2)", + ], [ PHP_VERSION_ID < 80300 ? 'array{2.0, 3.0, 4.0, 5.0}' : 'array{2, 3, 4, 5}', 'range(2, 5, 1.0)', @@ -5728,9 +5628,7 @@ public function dataRangeFunction(): array ]; } - /** - * @dataProvider dataRangeFunction - */ + #[DataProvider('dataRangeFunction')] public function testRangeFunction( string $description, string $expression, @@ -5743,7 +5641,7 @@ public function testRangeFunction( ); } - public function dataSpecifiedTypesUsingIsFunctions(): array + public static function dataSpecifiedTypesUsingIsFunctions(): array { return [ [ @@ -5775,7 +5673,7 @@ public function dataSpecifiedTypesUsingIsFunctions(): array '$null', ], [ - 'array', + 'array', '$array', ], [ @@ -5885,9 +5783,7 @@ public function dataSpecifiedTypesUsingIsFunctions(): array ]; } - /** - * @dataProvider dataSpecifiedTypesUsingIsFunctions - */ + #[DataProvider('dataSpecifiedTypesUsingIsFunctions')] public function testSpecifiedTypesUsingIsFunctions( string $description, string $expression, @@ -5900,7 +5796,7 @@ public function testSpecifiedTypesUsingIsFunctions( ); } - public function dataIterable(): array + public static function dataIterable(): array { return [ [ @@ -6050,9 +5946,7 @@ public function dataIterable(): array ]; } - /** - * @dataProvider dataIterable - */ + #[DataProvider('dataIterable')] public function testIterable( string $description, string $expression, @@ -6065,7 +5959,7 @@ public function testIterable( ); } - public function dataArrayAccess(): array + public static function dataArrayAccess(): array { return [ [ @@ -6087,9 +5981,7 @@ public function dataArrayAccess(): array ]; } - /** - * @dataProvider dataArrayAccess - */ + #[DataProvider('dataArrayAccess')] public function testArrayAccess( string $description, string $expression, @@ -6102,7 +5994,7 @@ public function testArrayAccess( ); } - public function dataVoid(): array + public static function dataVoid(): array { return [ [ @@ -6120,9 +6012,7 @@ public function dataVoid(): array ]; } - /** - * @dataProvider dataVoid - */ + #[DataProvider('dataVoid')] public function testVoid( string $description, string $expression, @@ -6135,7 +6025,7 @@ public function testVoid( ); } - public function dataNullableReturnTypes(): array + public static function dataNullableReturnTypes(): array { return [ [ @@ -6157,9 +6047,7 @@ public function dataNullableReturnTypes(): array ]; } - /** - * @dataProvider dataNullableReturnTypes - */ + #[DataProvider('dataNullableReturnTypes')] public function testNullableReturnTypes( string $description, string $expression, @@ -6172,7 +6060,7 @@ public function testNullableReturnTypes( ); } - public function dataTernary(): array + public static function dataTernary(): array { return [ [ @@ -6202,9 +6090,7 @@ public function dataTernary(): array ]; } - /** - * @dataProvider dataTernary - */ + #[DataProvider('dataTernary')] public function testTernary( string $description, string $expression, @@ -6217,7 +6103,7 @@ public function testTernary( ); } - public function dataHeredoc(): array + public static function dataHeredoc(): array { return [ [ @@ -6231,9 +6117,7 @@ public function dataHeredoc(): array ]; } - /** - * @dataProvider dataHeredoc - */ + #[DataProvider('dataHeredoc')] public function testHeredoc( string $description, string $expression, @@ -6246,7 +6130,7 @@ public function testHeredoc( ); } - public function dataTypeElimination(): array + public static function dataTypeElimination(): array { return [ [ @@ -6402,9 +6286,7 @@ public function dataTypeElimination(): array ]; } - /** - * @dataProvider dataTypeElimination - */ + #[DataProvider('dataTypeElimination')] public function testTypeElimination( string $description, string $expression, @@ -6419,7 +6301,7 @@ public function testTypeElimination( ); } - public function dataMisleadingTypes(): array + public static function dataMisleadingTypes(): array { return [ [ @@ -6431,15 +6313,13 @@ public function dataMisleadingTypes(): array '$foo->misleadingIntReturnType()', ], [ - 'mixed', + PHP_VERSION_ID >= 80000 ? 'mixed' : 'MisleadingTypes\mixed', '$foo->misleadingMixedReturnType()', ], ]; } - /** - * @dataProvider dataMisleadingTypes - */ + #[DataProvider('dataMisleadingTypes')] public function testMisleadingTypes( string $description, string $expression, @@ -6452,7 +6332,7 @@ public function testMisleadingTypes( ); } - public function dataMisleadingTypesWithoutNamespace(): array + public static function dataMisleadingTypesWithoutNamespace(): array { return [ [ @@ -6466,9 +6346,7 @@ public function dataMisleadingTypesWithoutNamespace(): array ]; } - /** - * @dataProvider dataMisleadingTypesWithoutNamespace - */ + #[DataProvider('dataMisleadingTypesWithoutNamespace')] public function testMisleadingTypesWithoutNamespace( string $description, string $expression, @@ -6481,7 +6359,7 @@ public function testMisleadingTypesWithoutNamespace( ); } - public function dataUnresolvableTypes(): array + public static function dataUnresolvableTypes(): array { return [ [ @@ -6499,9 +6377,7 @@ public function dataUnresolvableTypes(): array ]; } - /** - * @dataProvider dataUnresolvableTypes - */ + #[DataProvider('dataUnresolvableTypes')] public function testUnresolvableTypes( string $description, string $expression, @@ -6514,7 +6390,7 @@ public function testUnresolvableTypes( ); } - public function dataCombineTypes(): array + public static function dataCombineTypes(): array { return [ [ @@ -6528,9 +6404,7 @@ public function dataCombineTypes(): array ]; } - /** - * @dataProvider dataCombineTypes - */ + #[DataProvider('dataCombineTypes')] public function testCombineTypes( string $description, string $expression, @@ -6543,7 +6417,7 @@ public function testCombineTypes( ); } - public function dataConstants(): array + public static function dataConstants(): array { define('ConstantsForNodeScopeResolverTest\\FOO_CONSTANT', 1); @@ -6567,9 +6441,7 @@ public function dataConstants(): array ]; } - /** - * @dataProvider dataConstants - */ + #[DataProvider('dataConstants')] public function testConstants( string $description, string $expression, @@ -6582,7 +6454,7 @@ public function testConstants( ); } - public function dataFinally(): array + public static function dataFinally(): array { return [ [ @@ -6596,9 +6468,7 @@ public function dataFinally(): array ]; } - /** - * @dataProvider dataFinally - */ + #[DataProvider('dataFinally')] public function testFinally( string $description, string $expression, @@ -6611,9 +6481,7 @@ public function testFinally( ); } - /** - * @dataProvider dataFinally - */ + #[DataProvider('dataFinally')] public function testFinallyWithEarlyTermination( string $description, string $expression, @@ -6626,7 +6494,7 @@ public function testFinallyWithEarlyTermination( ); } - public function dataInheritDocFromInterface(): array + public static function dataInheritDocFromInterface(): array { return [ [ @@ -6636,9 +6504,7 @@ public function dataInheritDocFromInterface(): array ]; } - /** - * @dataProvider dataInheritDocFromInterface - */ + #[DataProvider('dataInheritDocFromInterface')] public function testInheritDocFromInterface( string $description, string $expression, @@ -6651,9 +6517,7 @@ public function testInheritDocFromInterface( ); } - /** - * @dataProvider dataInheritDocFromInterface - */ + #[DataProvider('dataInheritDocFromInterface')] public function testInheritDocWithoutCurlyBracesFromInterface( string $description, string $expression, @@ -6666,7 +6530,7 @@ public function testInheritDocWithoutCurlyBracesFromInterface( ); } - public function dataInheritDocFromInterface2(): array + public static function dataInheritDocFromInterface2(): array { return [ [ @@ -6676,9 +6540,7 @@ public function dataInheritDocFromInterface2(): array ]; } - /** - * @dataProvider dataInheritDocFromInterface2 - */ + #[DataProvider('dataInheritDocFromInterface2')] public function testInheritDocFromInterface2( string $description, string $expression, @@ -6692,9 +6554,7 @@ public function testInheritDocFromInterface2( ); } - /** - * @dataProvider dataInheritDocFromInterface2 - */ + #[DataProvider('dataInheritDocFromInterface2')] public function testInheritDocWithoutCurlyBracesFromInterface2( string $description, string $expression, @@ -6708,7 +6568,7 @@ public function testInheritDocWithoutCurlyBracesFromInterface2( ); } - public function dataInheritDocFromTrait(): array + public static function dataInheritDocFromTrait(): array { return [ [ @@ -6718,9 +6578,7 @@ public function dataInheritDocFromTrait(): array ]; } - /** - * @dataProvider dataInheritDocFromTrait - */ + #[DataProvider('dataInheritDocFromTrait')] public function testInheritDocFromTrait( string $description, string $expression, @@ -6733,9 +6591,7 @@ public function testInheritDocFromTrait( ); } - /** - * @dataProvider dataInheritDocFromTrait - */ + #[DataProvider('dataInheritDocFromTrait')] public function testInheritDocWithoutCurlyBracesFromTrait( string $description, string $expression, @@ -6748,7 +6604,7 @@ public function testInheritDocWithoutCurlyBracesFromTrait( ); } - public function dataInheritDocFromTrait2(): array + public static function dataInheritDocFromTrait2(): array { return [ [ @@ -6758,9 +6614,7 @@ public function dataInheritDocFromTrait2(): array ]; } - /** - * @dataProvider dataInheritDocFromTrait2 - */ + #[DataProvider('dataInheritDocFromTrait2')] public function testInheritDocFromTrait2( string $description, string $expression, @@ -6775,9 +6629,7 @@ public function testInheritDocFromTrait2( ); } - /** - * @dataProvider dataInheritDocFromTrait2 - */ + #[DataProvider('dataInheritDocFromTrait2')] public function testInheritDocWithoutCurlyBracesFromTrait2( string $description, string $expression, @@ -6792,7 +6644,7 @@ public function testInheritDocWithoutCurlyBracesFromTrait2( ); } - public function dataResolveStatic(): array + public static function dataResolveStatic(): array { return [ [ @@ -6818,9 +6670,7 @@ public function dataResolveStatic(): array ]; } - /** - * @dataProvider dataResolveStatic - */ + #[DataProvider('dataResolveStatic')] public function testResolveStatic( string $description, string $expression, @@ -6833,7 +6683,7 @@ public function testResolveStatic( ); } - public function dataLoopVariables(): array + public static function dataLoopVariables(): array { return [ [ @@ -6879,7 +6729,7 @@ public function dataLoopVariables(): array ]; } - public function dataForeachLoopVariables(): array + public static function dataForeachLoopVariables(): array { return [ [ @@ -6975,7 +6825,7 @@ public function dataForeachLoopVariables(): array ]; } - public function dataWhileLoopVariables(): array + public static function dataWhileLoopVariables(): array { return [ [ @@ -7011,7 +6861,7 @@ public function dataWhileLoopVariables(): array ]; } - public function dataForLoopVariables(): array + public static function dataForLoopVariables(): array { return [ [ @@ -7047,10 +6897,8 @@ public function dataForLoopVariables(): array ]; } - /** - * @dataProvider dataLoopVariables - * @dataProvider dataForeachLoopVariables - */ + #[DataProvider('dataLoopVariables')] + #[DataProvider('dataForeachLoopVariables')] public function testForeachLoopVariables( string $description, string $expression, @@ -7065,10 +6913,8 @@ public function testForeachLoopVariables( ); } - /** - * @dataProvider dataLoopVariables - * @dataProvider dataWhileLoopVariables - */ + #[DataProvider('dataLoopVariables')] + #[DataProvider('dataWhileLoopVariables')] public function testWhileLoopVariables( string $description, string $expression, @@ -7083,10 +6929,8 @@ public function testWhileLoopVariables( ); } - /** - * @dataProvider dataLoopVariables - * @dataProvider dataForLoopVariables - */ + #[DataProvider('dataLoopVariables')] + #[DataProvider('dataForLoopVariables')] public function testForLoopVariables( string $description, string $expression, @@ -7101,7 +6945,7 @@ public function testForLoopVariables( ); } - public function dataDoWhileLoopVariables(): array + public static function dataDoWhileLoopVariables(): array { return [ [ @@ -7193,9 +7037,7 @@ public function dataDoWhileLoopVariables(): array ]; } - /** - * @dataProvider dataDoWhileLoopVariables - */ + #[DataProvider('dataDoWhileLoopVariables')] public function testDoWhileLoopVariables( string $description, string $expression, @@ -7210,7 +7052,7 @@ public function testDoWhileLoopVariables( ); } - public function dataMultipleClassesInOneFile(): array + public static function dataMultipleClassesInOneFile(): array { return [ [ @@ -7226,9 +7068,7 @@ public function dataMultipleClassesInOneFile(): array ]; } - /** - * @dataProvider dataMultipleClassesInOneFile - */ + #[DataProvider('dataMultipleClassesInOneFile')] public function testMultipleClassesInOneFile( string $description, string $expression, @@ -7243,7 +7083,7 @@ public function testMultipleClassesInOneFile( ); } - public function dataCallingMultipleClassesInOneFile(): array + public static function dataCallingMultipleClassesInOneFile(): array { return [ [ @@ -7257,9 +7097,7 @@ public function dataCallingMultipleClassesInOneFile(): array ]; } - /** - * @dataProvider dataCallingMultipleClassesInOneFile - */ + #[DataProvider('dataCallingMultipleClassesInOneFile')] public function testCallingMultipleClassesInOneFile( string $description, string $expression, @@ -7272,7 +7110,7 @@ public function testCallingMultipleClassesInOneFile( ); } - public function dataExplode(): array + public static function dataExplode(): array { return [ [ @@ -7298,9 +7136,7 @@ public function dataExplode(): array ]; } - /** - * @dataProvider dataExplode - */ + #[DataProvider('dataExplode')] public function testExplode( string $description, string $expression, @@ -7313,7 +7149,7 @@ public function testExplode( ); } - public function dataArrayPointerFunctions(): array + public static function dataArrayPointerFunctions(): array { return [ [ @@ -7391,9 +7227,7 @@ public function dataArrayPointerFunctions(): array ]; } - /** - * @dataProvider dataArrayPointerFunctions - */ + #[DataProvider('dataArrayPointerFunctions')] public function testArrayPointerFunctions( string $description, string $expression, @@ -7406,11 +7240,11 @@ public function testArrayPointerFunctions( ); } - public function dataReplaceFunctions(): array + public static function dataReplaceFunctions(): array { return [ [ - 'non-falsy-string', + 'lowercase-string&non-falsy-string', '$expectedString', ], [ @@ -7418,7 +7252,7 @@ public function dataReplaceFunctions(): array '$expectedString2', ], [ - 'non-falsy-string|null', + '(lowercase-string&non-falsy-string)|null', '$anotherExpectedString', ], [ @@ -7426,11 +7260,11 @@ public function dataReplaceFunctions(): array '$expectedArray', ], [ - 'array{a: string, b: string}|null', + 'array{a?: string, b?: string}', '$expectedArray2', ], [ - 'array{a: string, b: string}|null', + 'array{a?: string, b?: string}', '$anotherExpectedArray', ], [ @@ -7450,7 +7284,7 @@ public function dataReplaceFunctions(): array '$anotherExpectedArrayOrString', ], [ - 'array{a: string, b: string}|null', + 'array{a?: string, b?: string}', 'preg_replace_callback_array($callbacks, $array)', ], [ @@ -7468,9 +7302,7 @@ public function dataReplaceFunctions(): array ]; } - /** - * @dataProvider dataReplaceFunctions - */ + #[DataProvider('dataReplaceFunctions')] public function testReplaceFunctions( string $description, string $expression, @@ -7483,7 +7315,10 @@ public function testReplaceFunctions( ); } - public function dataFilterVar(): Generator + /** + * @return Generator + */ + public static function dataFilterVar(): Generator { $typesAndFilters = [ 'string' => [ @@ -7590,7 +7425,7 @@ public function dataFilterVar(): Generator ]; } - public function dataFilterVarUnchanged(): array + public static function dataFilterVarUnchanged(): array { return [ [ @@ -7628,10 +7463,8 @@ public function dataFilterVarUnchanged(): array ]; } - /** - * @dataProvider dataFilterVar - * @dataProvider dataFilterVarUnchanged - */ + #[DataProvider('dataFilterVar')] + #[DataProvider('dataFilterVarUnchanged')] public function testFilterVar( string $description, string $expression, @@ -7644,7 +7477,7 @@ public function testFilterVar( ); } - public function dataClosureWithUsePassedByReference(): array + public static function dataClosureWithUsePassedByReference(): array { return [ [ @@ -7740,9 +7573,7 @@ public function dataClosureWithUsePassedByReference(): array ]; } - /** - * @dataProvider dataClosureWithUsePassedByReference - */ + #[DataProvider('dataClosureWithUsePassedByReference')] public function testClosureWithUsePassedByReference( string $description, string $expression, @@ -7757,7 +7588,7 @@ public function testClosureWithUsePassedByReference( ); } - public function dataClosureWithUsePassedByReferenceInMethodCall(): array + public static function dataClosureWithUsePassedByReferenceInMethodCall(): array { return [ [ @@ -7767,9 +7598,7 @@ public function dataClosureWithUsePassedByReferenceInMethodCall(): array ]; } - /** - * @dataProvider dataClosureWithUsePassedByReferenceInMethodCall - */ + #[DataProvider('dataClosureWithUsePassedByReferenceInMethodCall')] public function testClosureWithUsePassedByReferenceInMethodCall( string $description, string $expression, @@ -7782,7 +7611,7 @@ public function testClosureWithUsePassedByReferenceInMethodCall( ); } - public function dataClosureWithUsePassedByReferenceReturn(): array + public static function dataClosureWithUsePassedByReferenceReturn(): array { return [ [ @@ -7808,7 +7637,7 @@ public function dataClosureWithUsePassedByReferenceReturn(): array ]; } - public function dataStaticClosure(): array + public static function dataStaticClosure(): array { return [ [ @@ -7818,9 +7647,7 @@ public function dataStaticClosure(): array ]; } - /** - * @dataProvider dataStaticClosure - */ + #[DataProvider('dataStaticClosure')] public function testStaticClosure( string $description, string $expression, @@ -7833,9 +7660,7 @@ public function testStaticClosure( ); } - /** - * @dataProvider dataClosureWithUsePassedByReferenceReturn - */ + #[DataProvider('dataClosureWithUsePassedByReferenceReturn')] public function testClosureWithUsePassedByReferenceReturn( string $description, string $expression, @@ -7850,7 +7675,7 @@ public function testClosureWithUsePassedByReferenceReturn( ); } - public function dataClosureWithInferredTypehint(): array + public static function dataClosureWithInferredTypehint(): array { return [ [ @@ -7864,9 +7689,7 @@ public function dataClosureWithInferredTypehint(): array ]; } - /** - * @dataProvider dataClosureWithInferredTypehint - */ + #[DataProvider('dataClosureWithInferredTypehint')] public function testClosureWithInferredTypehint( string $description, string $expression, @@ -7882,7 +7705,7 @@ public function testClosureWithInferredTypehint( ); } - public function dataTraitsPhpDocs(): array + public static function dataTraitsPhpDocs(): array { return [ [ @@ -7972,9 +7795,7 @@ public function dataTraitsPhpDocs(): array ]; } - /** - * @dataProvider dataTraitsPhpDocs - */ + #[DataProvider('dataTraitsPhpDocs')] public function testTraitsPhpDocs( string $description, string $expression, @@ -7987,7 +7808,7 @@ public function testTraitsPhpDocs( ); } - public function dataPassedByReference(): array + public static function dataPassedByReference(): array { return [ [ @@ -8005,9 +7826,7 @@ public function dataPassedByReference(): array ]; } - /** - * @dataProvider dataPassedByReference - */ + #[DataProvider('dataPassedByReference')] public function testPassedByReference( string $description, string $expression, @@ -8020,7 +7839,7 @@ public function testPassedByReference( ); } - public function dataCallables(): array + public static function dataCallables(): array { return [ [ @@ -8050,9 +7869,7 @@ public function dataCallables(): array ]; } - /** - * @dataProvider dataCallables - */ + #[DataProvider('dataCallables')] public function testCallables( string $description, string $expression, @@ -8065,7 +7882,7 @@ public function testCallables( ); } - public function dataArrayKeysInBranches(): array + public static function dataArrayKeysInBranches(): array { return [ [ @@ -8073,11 +7890,11 @@ public function dataArrayKeysInBranches(): array '$array', ], [ - 'array&hasOffsetValue(\'key\', mixed)', + 'non-empty-array&hasOffsetValue(\'key\', mixed~null)', '$generalArray', ], [ - 'mixed', + 'mixed~null', '$generalArray[\'key\']', ], [ @@ -8089,7 +7906,7 @@ public function dataArrayKeysInBranches(): array '$arrayAppendedInForeach', ], [ - 'non-empty-array, literal-string&non-falsy-string>', // could be 'array, \'bar\'|\'baz\'|\'foo\'>' + 'non-empty-array, literal-string&lowercase-string&non-falsy-string>', // could be 'array, \'bar\'|\'baz\'|\'foo\'>' '$anotherArrayAppendedInForeach', ], [ @@ -8107,9 +7924,7 @@ public function dataArrayKeysInBranches(): array ]; } - /** - * @dataProvider dataArrayKeysInBranches - */ + #[DataProvider('dataArrayKeysInBranches')] public function testArrayKeysInBranches( string $description, string $expression, @@ -8122,7 +7937,7 @@ public function testArrayKeysInBranches( ); } - public function dataSpecifiedFunctionCall(): array + public static function dataSpecifiedFunctionCall(): array { return [ [ @@ -8153,9 +7968,7 @@ public function dataSpecifiedFunctionCall(): array ]; } - /** - * @dataProvider dataSpecifiedFunctionCall - */ + #[DataProvider('dataSpecifiedFunctionCall')] public function testSpecifiedFunctionCall( string $description, string $expression, @@ -8170,7 +7983,7 @@ public function testSpecifiedFunctionCall( ); } - public function dataElementsOnMixed(): array + public static function dataElementsOnMixed(): array { return [ [ @@ -8196,9 +8009,7 @@ public function dataElementsOnMixed(): array ]; } - /** - * @dataProvider dataElementsOnMixed - */ + #[DataProvider('dataElementsOnMixed')] public function testElementsOnMixed( string $description, string $expression, @@ -8211,7 +8022,7 @@ public function testElementsOnMixed( ); } - public function dataCaseInsensitivePhpDocTypes(): array + public static function dataCaseInsensitivePhpDocTypes(): array { return [ [ @@ -8225,9 +8036,7 @@ public function dataCaseInsensitivePhpDocTypes(): array ]; } - /** - * @dataProvider dataCaseInsensitivePhpDocTypes - */ + #[DataProvider('dataCaseInsensitivePhpDocTypes')] public function testCaseInsensitivePhpDocTypes( string $description, string $expression, @@ -8240,7 +8049,7 @@ public function testCaseInsensitivePhpDocTypes( ); } - public function dataConstantTypeAfterDuplicateCondition(): array + public static function dataConstantTypeAfterDuplicateCondition(): array { return [ [ @@ -8306,9 +8115,7 @@ public function dataConstantTypeAfterDuplicateCondition(): array ]; } - /** - * @dataProvider dataConstantTypeAfterDuplicateCondition - */ + #[DataProvider('dataConstantTypeAfterDuplicateCondition')] public function testConstantTypeAfterDuplicateCondition( string $description, string $expression, @@ -8323,7 +8130,7 @@ public function testConstantTypeAfterDuplicateCondition( ); } - public function dataAnonymousClass(): array + public static function dataAnonymousClass(): array { return [ [ @@ -8359,9 +8166,7 @@ public function dataAnonymousClass(): array ]; } - /** - * @dataProvider dataAnonymousClass - */ + #[DataProvider('dataAnonymousClass')] public function testAnonymousClassName( string $description, string $expression, @@ -8376,7 +8181,7 @@ public function testAnonymousClassName( ); } - public function dataAnonymousClassInTrait(): array + public static function dataAnonymousClassInTrait(): array { return [ [ @@ -8386,9 +8191,7 @@ public function dataAnonymousClassInTrait(): array ]; } - /** - * @dataProvider dataAnonymousClassInTrait - */ + #[DataProvider('dataAnonymousClassInTrait')] public function testAnonymousClassNameInTrait( string $description, string $expression, @@ -8401,7 +8204,7 @@ public function testAnonymousClassNameInTrait( ); } - public function dataAnonymousClassNameSameLine(): array + public static function dataAnonymousClassNameSameLine(): array { return [ [ @@ -8422,9 +8225,7 @@ public function dataAnonymousClassNameSameLine(): array ]; } - /** - * @dataProvider dataAnonymousClassNameSameLine - */ + #[DataProvider('dataAnonymousClassNameSameLine')] public function testAnonymousClassNameSameLine( string $description, string $expression, @@ -8439,7 +8240,7 @@ public function testAnonymousClassNameSameLine( ); } - public function dataDynamicConstants(): array + public static function dataDynamicConstants(): array { return [ [ @@ -8465,9 +8266,7 @@ public function dataDynamicConstants(): array ]; } - /** - * @dataProvider dataDynamicConstants - */ + #[DataProvider('dataDynamicConstants')] public function testDynamicConstants( string $description, string $expression, @@ -8485,7 +8284,7 @@ public function testDynamicConstants( ); } - public function dataDynamicConstantsWithNativeTypes(): array + public static function dataDynamicConstantsWithNativeTypes(): array { return [ [ @@ -8507,18 +8306,13 @@ public function dataDynamicConstantsWithNativeTypes(): array ]; } - /** - * @dataProvider dataDynamicConstantsWithNativeTypes - */ + #[RequiresPhp('>= 8.3')] + #[DataProvider('dataDynamicConstantsWithNativeTypes')] public function testDynamicConstantsWithNativeTypes( string $description, string $expression, ): void { - if (PHP_VERSION_ID < 80300) { - $this->markTestSkipped('Test requires PHP 8.3.'); - } - $this->assertTypes( __DIR__ . '/data/dynamic-constant-native-types.php', $description, @@ -8531,7 +8325,7 @@ public function testDynamicConstantsWithNativeTypes( ); } - public function dataIsset(): array + public static function dataIsset(): array { return [ [ @@ -8559,11 +8353,11 @@ public function dataIsset(): array '$mixedIsset', ], [ - 'array&hasOffset(\'a\')', + 'non-empty-array&hasOffset(\'a\')', '$mixedArrayKeyExists', ], [ - 'array&hasOffsetValue(\'a\', int)', + 'non-empty-array&hasOffsetValue(\'a\', int)', '$integers', ], [ @@ -8597,9 +8391,7 @@ public function dataIsset(): array ]; } - /** - * @dataProvider dataIsset - */ + #[DataProvider('dataIsset')] public function testIsset( string $description, string $expression, @@ -8612,7 +8404,7 @@ public function testIsset( ); } - public function dataPropertyArrayAssignment(): array + public static function dataPropertyArrayAssignment(): array { return [ [ @@ -8643,9 +8435,7 @@ public function dataPropertyArrayAssignment(): array ]; } - /** - * @dataProvider dataPropertyArrayAssignment - */ + #[DataProvider('dataPropertyArrayAssignment')] public function testPropertyArrayAssignment( string $description, string $expression, @@ -8660,7 +8450,7 @@ public function testPropertyArrayAssignment( ); } - public function dataGetParentClass(): array + public static function dataGetParentClass(): array { return [ [ @@ -8734,9 +8524,7 @@ public function dataGetParentClass(): array ]; } - /** - * @dataProvider dataGetParentClass - */ + #[DataProvider('dataGetParentClass')] public function testGetParentClass( string $description, string $expression, @@ -8751,7 +8539,7 @@ public function testGetParentClass( ); } - public function dataIsCountable(): array + public static function dataIsCountable(): array { return [ [ @@ -8767,9 +8555,7 @@ public function dataIsCountable(): array ]; } - /** - * @dataProvider dataIsCountable - */ + #[DataProvider('dataIsCountable')] public function testIsCountable( string $description, string $expression, @@ -8784,7 +8570,7 @@ public function testIsCountable( ); } - public function dataPhp73Functions(): array + public static function dataPhp73Functions(): array { return [ [ @@ -8906,17 +8692,12 @@ public function dataPhp73Functions(): array ]; } - /** - * @dataProvider dataPhp73Functions - */ + #[DataProvider('dataPhp73Functions')] 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, @@ -8924,139 +8705,7 @@ public function testPhp73Functions( ); } - public function dataPhp74Functions(): array - { - return [ - [ - PHP_VERSION_ID < 80000 ? 'list|false' : 'list', - '$mbStrSplitConstantStringWithoutDefinedParameters', - ], - [ - 'array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', - '$mbStrSplitConstantStringWithoutDefinedSplitLength', - ], - [ - 'list', - '$mbStrSplitStringWithoutDefinedSplitLength', - ], - [ - 'array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', - '$mbStrSplitConstantStringWithOneSplitLength', - ], - [ - 'array{\'abcdef\'}', - '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLength', - ], - [ - 'false', - '$mbStrSplitConstantStringWithFailureSplitLength', - ], - [ - PHP_VERSION_ID < 80000 ? 'list|false' : 'list', - '$mbStrSplitConstantStringWithInvalidSplitLengthType', - ], - [ - "array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", - '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLength', - ], - [ - PHP_VERSION_ID < 80000 ? 'list|false' : 'list', - '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLength', - ], - [ - "array{'a', 'b', 'c', 'd', 'e', 'f'}", - '$mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding', - ], - [ - 'false', - '$mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding', - ], - [ - PHP_VERSION_ID < 80000 ? 'list|false' : 'list', - '$mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding', - ], - [ - "array{'abcdef'}", - '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndValidEncoding', - ], - [ - 'false', - '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding', - ], - [ - PHP_VERSION_ID < 80000 ? 'list|false' : 'list', - '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding', - ], - [ - 'false', - '$mbStrSplitConstantStringWithFailureSplitLengthAndValidEncoding', - ], - [ - 'false', - '$mbStrSplitConstantStringWithFailureSplitLengthAndInvalidEncoding', - ], - [ - 'false', - '$mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding', - ], - [ - PHP_VERSION_ID < 80000 ? 'list|false' : 'list', - '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding', - ], - [ - 'false', - '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding', - ], - [ - PHP_VERSION_ID < 80000 ? 'list|false' : 'list', - '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding', - ], - [ - "array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", - '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding', - ], - [ - 'false', - '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding', - ], - [ - PHP_VERSION_ID < 80000 ? 'list|false' : 'list', - '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding', - ], - [ - PHP_VERSION_ID < 80000 ? 'list|false' : 'list', - '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding', - ], - [ - 'false', - '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding', - ], - [ - PHP_VERSION_ID < 80000 ? 'list|false' : 'list', - '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding', - ], - ]; - } - - /** - * @dataProvider dataPhp74Functions - */ - public function testPhp74Functions( - string $description, - string $expression, - ): void - { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4'); - } - $this->assertTypes( - __DIR__ . '/data/php74_functions.php', - $description, - $expression, - ); - } - - public function dataUnionMethods(): array + public static function dataUnionMethods(): array { return [ [ @@ -9070,9 +8719,7 @@ public function dataUnionMethods(): array ]; } - /** - * @dataProvider dataUnionMethods - */ + #[DataProvider('dataUnionMethods')] public function testUnionMethods( string $description, string $expression, @@ -9085,7 +8732,7 @@ public function testUnionMethods( ); } - public function dataUnionProperties(): array + public static function dataUnionProperties(): array { return [ [ @@ -9099,9 +8746,7 @@ public function dataUnionProperties(): array ]; } - /** - * @dataProvider dataUnionProperties - */ + #[DataProvider('dataUnionProperties')] public function testUnionProperties( string $description, string $expression, @@ -9114,7 +8759,7 @@ public function testUnionProperties( ); } - public function dataAssignmentInCondition(): array + public static function dataAssignmentInCondition(): array { return [ [ @@ -9124,9 +8769,7 @@ public function dataAssignmentInCondition(): array ]; } - /** - * @dataProvider dataAssignmentInCondition - */ + #[DataProvider('dataAssignmentInCondition')] public function testAssignmentInCondition( string $description, string $expression, @@ -9139,19 +8782,17 @@ public function testAssignmentInCondition( ); } - public function dataGeneralizeScope(): array + public static function dataGeneralizeScope(): array { return [ [ - 'array, removeCount: int<0, max>, loadCount: int<0, max>, hitCount: int<0, max>}>>', + 'array, removeCount: int<0, max>, loadCount: int<0, max>, hitCount: int<0, max>}>>', '$statistics', ], ]; } - /** - * @dataProvider dataGeneralizeScope - */ + #[DataProvider('dataGeneralizeScope')] public function testGeneralizeScope( string $description, string $expression, @@ -9164,7 +8805,7 @@ public function testGeneralizeScope( ); } - public function dataGeneralizeScopeRecursiveType(): array + public static function dataGeneralizeScopeRecursiveType(): array { return [ [ @@ -9174,9 +8815,7 @@ public function dataGeneralizeScopeRecursiveType(): array ]; } - /** - * @dataProvider dataGeneralizeScopeRecursiveType - */ + #[DataProvider('dataGeneralizeScopeRecursiveType')] public function testGeneralizeScopeRecursiveType( string $description, string $expression, @@ -9189,7 +8828,7 @@ public function testGeneralizeScopeRecursiveType( ); } - public function dataArrayShapesInPhpDoc(): array + public static function dataArrayShapesInPhpDoc(): array { return [ [ @@ -9207,9 +8846,7 @@ public function dataArrayShapesInPhpDoc(): array ]; } - /** - * @dataProvider dataArrayShapesInPhpDoc - */ + #[DataProvider('dataArrayShapesInPhpDoc')] public function testArrayShapesInPhpDoc( string $description, string $expression, @@ -9222,7 +8859,7 @@ public function testArrayShapesInPhpDoc( ); } - public function dataInferPrivatePropertyTypeFromConstructor(): array + public static function dataInferPrivatePropertyTypeFromConstructor(): array { return [ [ @@ -9254,15 +8891,13 @@ public function dataInferPrivatePropertyTypeFromConstructor(): array '$this->bool', ], [ - 'array', + 'array', '$this->array', ], ]; } - /** - * @dataProvider dataInferPrivatePropertyTypeFromConstructor - */ + #[DataProvider('dataInferPrivatePropertyTypeFromConstructor')] public function testInferPrivatePropertyTypeFromConstructor( string $description, string $expression, @@ -9275,7 +8910,7 @@ public function testInferPrivatePropertyTypeFromConstructor( ); } - public function dataPropertyNativeTypes(): array + public static function dataPropertyNativeTypes(): array { return [ [ @@ -9293,9 +8928,7 @@ public function dataPropertyNativeTypes(): array ]; } - /** - * @dataProvider dataPropertyNativeTypes - */ + #[DataProvider('dataPropertyNativeTypes')] public function testPropertyNativeTypes( string $description, string $expression, @@ -9308,7 +8941,7 @@ public function testPropertyNativeTypes( ); } - public function dataArrowFunctions(): array + public static function dataArrowFunctions(): array { return [ [ @@ -9326,9 +8959,7 @@ public function dataArrowFunctions(): array ]; } - /** - * @dataProvider dataArrowFunctions - */ + #[DataProvider('dataArrowFunctions')] public function testArrowFunctions( string $description, string $expression, @@ -9341,7 +8972,7 @@ public function testArrowFunctions( ); } - public function dataArrowFunctionsInside(): array + public static function dataArrowFunctionsInside(): array { return [ [ @@ -9359,9 +8990,7 @@ public function dataArrowFunctionsInside(): array ]; } - /** - * @dataProvider dataArrowFunctionsInside - */ + #[DataProvider('dataArrowFunctionsInside')] public function testArrowFunctionsInside( string $description, string $expression, @@ -9374,7 +9003,7 @@ public function testArrowFunctionsInside( ); } - public function dataCoalesceAssign(): array + public static function dataCoalesceAssign(): array { return [ [ @@ -9416,9 +9045,7 @@ public function dataCoalesceAssign(): array ]; } - /** - * @dataProvider dataCoalesceAssign - */ + #[DataProvider('dataCoalesceAssign')] public function testCoalesceAssign( string $description, string $expression, @@ -9431,7 +9058,7 @@ public function testCoalesceAssign( ); } - public function dataArraySpread(): array + public static function dataArraySpread(): array { return [ [ @@ -9465,9 +9092,7 @@ public function dataArraySpread(): array ]; } - /** - * @dataProvider dataArraySpread - */ + #[DataProvider('dataArraySpread')] public function testArraySpread( string $description, string $expression, @@ -9480,35 +9105,7 @@ public function testArraySpread( ); } - public function dataPhp74FunctionsIn73(): array - { - return [ - [ - 'mixed', - 'password_algos()', - ], - ]; - } - - /** - * @dataProvider dataPhp74FunctionsIn73 - */ - public function testPhp74FunctionsIn73( - string $description, - string $expression, - ): void - { - if (PHP_VERSION_ID >= 70400) { - $this->markTestSkipped('Test does not run on PHP >= 7.4.'); - } - $this->assertTypes( - __DIR__ . '/data/die-73.php', - $description, - $expression, - ); - } - - public function dataPhp74FunctionsIn74(): array + public static function dataPhp74FunctionsIn74(): array { return [ [ @@ -9518,17 +9115,12 @@ public function dataPhp74FunctionsIn74(): array ]; } - /** - * @dataProvider dataPhp74FunctionsIn74 - */ + #[DataProvider('dataPhp74FunctionsIn74')] public function testPhp74FunctionsIn74( string $description, string $expression, ): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->assertTypes( __DIR__ . '/data/die-74.php', $description, @@ -9536,7 +9128,7 @@ public function testPhp74FunctionsIn74( ); } - public function dataTryCatchScope(): array + public static function dataTryCatchScope(): array { return [ [ @@ -9557,9 +9149,7 @@ public function dataTryCatchScope(): array ]; } - /** - * @dataProvider dataTryCatchScope - */ + #[DataProvider('dataTryCatchScope')] public function testTryCatchScope( string $description, string $expression, @@ -9631,7 +9221,7 @@ public static function getAdditionalConfigFiles(): array ]; } - public function dataDeclareStrictTypes(): array + public static function dataDeclareStrictTypes(): array { return [ [ @@ -9649,9 +9239,7 @@ public function dataDeclareStrictTypes(): array ]; } - /** - * @dataProvider dataDeclareStrictTypes - */ + #[DataProvider('dataDeclareStrictTypes')] public function testDeclareStrictTypes(string $file, bool $result): void { self::processFile($file, function (Node $node, Scope $scope) use ($result): void { diff --git a/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php b/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php index 83bb27b70b..3b4f48d07a 100644 --- a/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php +++ b/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class LooseConstComparisonPhp7Test extends TypeInferenceTestCase { @@ -10,17 +11,17 @@ class LooseConstComparisonPhp7Test extends TypeInferenceTestCase /** * @return iterable> */ - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { // compares constants according to the php-version phpstan configuration, // _NOT_ the current php runtime version - yield from $this->gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php7.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php7.php'); } /** - * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, @@ -33,7 +34,7 @@ public function testFileAsserts( public static function getAdditionalConfigFiles(): array { return [ - __DIR__ . '/looseConstComparisonPhp7.neon', + __DIR__ . '/nodeScopeResolverPhp7.neon', ]; } diff --git a/tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php b/tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php index e765ca01d5..fae702cec2 100644 --- a/tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php +++ b/tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class LooseConstComparisonPhp8Test extends TypeInferenceTestCase { @@ -10,17 +11,17 @@ class LooseConstComparisonPhp8Test extends TypeInferenceTestCase /** * @return iterable> */ - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { // compares constants according to the php-version phpstan configuration, // _NOT_ the current php runtime version - yield from $this->gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php8.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php8.php'); } /** - * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, @@ -33,7 +34,7 @@ public function testFileAsserts( public static function getAdditionalConfigFiles(): array { return [ - __DIR__ . '/looseConstComparisonPhp8.neon', + __DIR__ . '/nodeScopeResolverPhp8.neon', ]; } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 6a8c6b517d..516a68397e 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -3,77 +3,113 @@ 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 PHP_INT_SIZE; use const PHP_VERSION_ID; class NodeScopeResolverTest extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + /** + * @return iterable + */ + private static function findTestFiles(): iterable { - yield from $this->gatherAssertTypesFromDirectory(__DIR__ . '/nsrt'); + foreach (self::findTestDataFilesFromDirectory(__DIR__ . '/nsrt') as $testFile) { + yield $testFile; + } if (PHP_VERSION_ID < 80200 && PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/enum-reflection-php81.php'); + yield __DIR__ . '/data/enum-reflection-php81.php'; + } + + if (PHP_VERSION_ID >= 80100 && PHP_VERSION_ID < 80400) { + yield __DIR__ . '/data/enum-reflection-backed.php'; } - if (PHP_VERSION_ID < 80000 && PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4902.php'); + if (PHP_VERSION_ID < 80000) { + yield __DIR__ . '/data/bug-4902.php'; } if (PHP_VERSION_ID < 80300) { if (PHP_VERSION_ID >= 80200) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/mb-strlen-php82.php'); + yield __DIR__ . '/data/mb-strlen-php82.php'; } elseif (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/mb-strlen-php8.php'); - } elseif (PHP_VERSION_ID < 70300) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/mb-strlen-php72.php'); + yield __DIR__ . '/data/mb-strlen-php8.php'; } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/mb-strlen-php73.php'); + yield __DIR__ . '/data/mb-strlen-php73.php'; } } - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-6856.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 __DIR__ . '/../Rules/Methods/data/bug-6856.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__ . '/../Reflection/data/unionTypes.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Reflection/data/mixedType.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Reflection/data/staticReturnType.php'); + yield __DIR__ . '/../Reflection/data/unionTypes.php'; + yield __DIR__ . '/../Reflection/data/mixedType.php'; + yield __DIR__ . '/../Reflection/data/staticReturnType.php'; } if (PHP_INT_SIZE === 8) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants-64bit.php'); + yield __DIR__ . '/data/predefined-constants-64bit.php'; } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants-32bit.php'); + yield __DIR__ . '/data/predefined-constants-32bit.php'; } - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Variables/data/bug-10577.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Variables/data/bug-10610.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-2550.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Properties/data/bug-3777.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-4552.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/infer-array-key.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Generics/data/bug-3769.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Generics/data/bug-6301.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/PhpDoc/data/bug-4643.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'; if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-4857.php'); + yield __DIR__ . '/../Rules/Comparison/data/bug-4857.php'; } - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5089.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/unable-to-resolve-callback-parameter-type.php'); + yield __DIR__ . '/../Rules/Methods/data/bug-5089.php'; + yield __DIR__ . '/../Rules/Methods/data/unable-to-resolve-callback-parameter-type.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/varying-acceptor.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-4415.php'); - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5372.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-5372_2.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5562.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()); @@ -82,118 +118,174 @@ public function dataFileAsserts(): iterable define('TEST_FALSE_CONSTANT', false); define('TEST_ARRAY_CONSTANT', [true, false, null]); define('TEST_ENUM_CONSTANT', Foo::ONE); - yield from $this->gatherAssertTypes(__DIR__ . '/data/new-in-initializers-runtime.php'); + yield __DIR__ . '/data/new-in-initializers-runtime.php'; + yield __DIR__ . '/data/scope-in-enum-match-arm-body.php'; } - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-6473.php'); - } + yield __DIR__ . '/../Rules/Comparison/data/bug-6473.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/filter-iterator-child-class.php'); + yield __DIR__ . '/../Rules/Methods/data/filter-iterator-child-class.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5749.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5757.php'); + yield __DIR__ . '/../Rules/Methods/data/bug-5749.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-5757.php'; if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-6635.php'); + yield __DIR__ . '/../Rules/Methods/data/bug-6635.php'; } if (PHP_VERSION_ID >= 80300) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Constants/data/bug-10212.php'); + yield __DIR__ . '/../Rules/Constants/data/bug-10212.php'; } - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-3284.php'); + yield __DIR__ . '/../Rules/Methods/data/bug-3284.php'; if (PHP_VERSION_ID >= 80300) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/return-type-class-constant.php'); + yield __DIR__ . '/../Rules/Methods/data/return-type-class-constant.php'; } //define('ALREADY_DEFINED_CONSTANT', true); - //yield from $this->gatherAssertTypes(__DIR__ . '/data/already-defined-constant.php'); + //yield from self::gatherAssertTypes(__DIR__ . '/data/already-defined-constant.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/conditional-complex-templates.php'); + yield __DIR__ . '/../Rules/Methods/data/conditional-complex-templates.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-7511.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-4708.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-7156.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-6364.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-5758.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-3931.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Variables/data/bug-7417.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-7469.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Variables/data/bug-3391.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 >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-anonymous-function-method-constant.php'); - } + yield __DIR__ . '/../Rules/Functions/data/bug-anonymous-function-method-constant.php'; if (PHP_VERSION_ID >= 80200) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/true-typehint.php'); + yield __DIR__ . '/../Rules/Methods/data/true-typehint.php'; } - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-6000.php'); + yield __DIR__ . '/../Rules/Arrays/data/bug-6000.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/slevomat-foreach-unset-bug.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php'); + yield __DIR__ . '/../Rules/Arrays/data/slevomat-foreach-unset-bug.php'; + yield __DIR__ . '/../Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php'; if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-7898.php'); + yield __DIR__ . '/../Rules/Comparison/data/bug-7898.php'; } if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-7823.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/../Analyser/data/is-resource-specified.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-7954.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/docblock-assert-equality.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Properties/data/bug-7839.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Classes/data/bug-5333.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-8174.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-8169.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-8280.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-8277.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Variables/data/bug-8113.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-8389.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-8467a.php'); + yield __DIR__ . '/../Rules/Functions/data/bug-7823.php'; + } + + yield __DIR__ . '/../Analyser/data/is-resource-specified.php'; + + yield __DIR__ . '/../Rules/Arrays/data/bug-7954.php'; + yield __DIR__ . '/../Rules/Comparison/data/docblock-assert-equality.php'; + yield __DIR__ . '/../Rules/Properties/data/bug-7839.php'; + yield __DIR__ . '/../Rules/Classes/data/bug-5333.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-8174.php'; + + if (PHP_VERSION_ID >= 80000) { + yield __DIR__ . '/../Rules/Comparison/data/bug-8169.php'; + } + yield __DIR__ . '/../Rules/Functions/data/bug-8280.php'; + yield __DIR__ . '/../Rules/Comparison/data/bug-8277.php'; + yield __DIR__ . '/../Rules/Variables/data/bug-8113.php'; + yield __DIR__ . '/../Rules/Functions/data/bug-8389.php'; + yield __DIR__ . '/../Rules/Arrays/data/bug-8467a.php'; + if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-8485.php'); + yield __DIR__ . '/../Rules/Comparison/data/bug-8485.php'; } if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-9007.php'); + yield __DIR__ . '/../Rules/Comparison/data/bug-9007.php'; } - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/DeadCode/data/bug-8620.php'); + yield __DIR__ . '/../Rules/DeadCode/data/bug-8620.php'; if (PHP_VERSION_ID >= 80200) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Constants/data/bug-8957.php'); + yield __DIR__ . '/../Rules/Constants/data/bug-8957.php'; } if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-9499.php'); + yield __DIR__ . '/../Rules/Comparison/data/bug-9499.php'; } - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/PhpDoc/data/bug-8609-function.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-5365.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-6551.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Variables/data/bug-9403.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-9542.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-9803.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/PhpDoc/data/bug-10594.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'; } /** - * @dataProvider dataFileAsserts - * @param mixed ...$args + * @return iterable */ - public function testFileAsserts( - string $assertType, - string $file, - ...$args, - ): void + public static function dataFile(): iterable + { + $base = dirname(__DIR__, 3) . '/'; + $baseLength = strlen($base); + + $fileHelper = new FileHelper($base); + foreach (self::findTestFiles() as $file) { + $file = $fileHelper->normalizePath($file); + + $testName = $file; + if (str_starts_with($file, $base)) { + $testName = substr($file, $baseLength); + } + + yield $testName => [$file]; + } + } + + #[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 index 795707aabb..cd28ff80f7 100644 --- a/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php +++ b/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php @@ -3,19 +3,20 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class ParamClosureThisStubsTest extends TypeInferenceTestCase { - public function dataAsserts(): iterable + public static function dataAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/param-closure-this-stubs.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/param-closure-this-stubs.php'); } /** - * @dataProvider dataAsserts * @param mixed ...$args */ + #[DataProvider('dataAsserts')] public function testAsserts( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/ParamOutTypeTest.php b/tests/PHPStan/Analyser/ParamOutTypeTest.php index b29b6fbd0c..251479a95b 100644 --- a/tests/PHPStan/Analyser/ParamOutTypeTest.php +++ b/tests/PHPStan/Analyser/ParamOutTypeTest.php @@ -3,19 +3,20 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class ParamOutTypeTest extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/param-out.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/param-out.php'); } /** - * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/ParameterClosureTypeExtensionArrowFunctionTest.php b/tests/PHPStan/Analyser/ParameterClosureTypeExtensionArrowFunctionTest.php index 025513c4bf..778dbf90f7 100644 --- a/tests/PHPStan/Analyser/ParameterClosureTypeExtensionArrowFunctionTest.php +++ b/tests/PHPStan/Analyser/ParameterClosureTypeExtensionArrowFunctionTest.php @@ -3,34 +3,26 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\DataProvider; class ParameterClosureTypeExtensionArrowFunctionTest extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { - if (PHP_VERSION_ID < 70400) { - return []; - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/parameter-closure-type-extension-arrow-function.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/parameter-closure-type-extension-arrow-function.php'); } /** - * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, ...$args, ): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->assertFileAsserts($assertType, $file, ...$args); } diff --git a/tests/PHPStan/Analyser/ParameterClosureTypeExtensionTest.php b/tests/PHPStan/Analyser/ParameterClosureTypeExtensionTest.php index 4b990f1f72..6079115500 100644 --- a/tests/PHPStan/Analyser/ParameterClosureTypeExtensionTest.php +++ b/tests/PHPStan/Analyser/ParameterClosureTypeExtensionTest.php @@ -3,19 +3,20 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class ParameterClosureTypeExtensionTest extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/parameter-closure-type-extension.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/parameter-closure-type-extension.php'); } /** - * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/ParameterOutTypeExtensionTest.php b/tests/PHPStan/Analyser/ParameterOutTypeExtensionTest.php index e32c7c3ca3..3ad3d0e1f0 100644 --- a/tests/PHPStan/Analyser/ParameterOutTypeExtensionTest.php +++ b/tests/PHPStan/Analyser/ParameterOutTypeExtensionTest.php @@ -3,19 +3,20 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class ParameterOutTypeExtensionTest extends TypeInferenceTestCase { - public function dataAsserts(): iterable + public static function dataAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/param-out/parameter-out-types.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/param-out/parameter-out-types.php'); } /** - * @dataProvider dataAsserts * @param mixed ...$args */ + #[DataProvider('dataAsserts')] public function testAsserts( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/PathConstantsTest.php b/tests/PHPStan/Analyser/PathConstantsTest.php index 39832befa4..3a3e124e58 100644 --- a/tests/PHPStan/Analyser/PathConstantsTest.php +++ b/tests/PHPStan/Analyser/PathConstantsTest.php @@ -3,24 +3,25 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; use const DIRECTORY_SEPARATOR; class PathConstantsTest extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { if (DIRECTORY_SEPARATOR === '\\') { - yield from $this->gatherAssertTypes(__DIR__ . '/data/pathConstants-win.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/pathConstants-win.php'); } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/pathConstants.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/pathConstants.php'); } } /** - * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, 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 99bba5ea9b..e83ed19354 100644 --- a/tests/PHPStan/Analyser/ScopeTest.php +++ b/tests/PHPStan/Analyser/ScopeTest.php @@ -5,20 +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 [ [ @@ -29,7 +32,7 @@ public function dataGeneralize(): array [ new ConstantStringType('a'), new ConstantStringType('b'), - 'literal-string&non-falsy-string', + 'literal-string&lowercase-string&non-falsy-string', ], [ new ConstantIntegerType(0), @@ -139,7 +142,7 @@ public function dataGeneralize(): array new ConstantIntegerType(1), new ConstantIntegerType(1), ]), - 'non-empty-array', + 'non-empty-array', ], [ new ConstantArrayType([ @@ -154,7 +157,7 @@ public function dataGeneralize(): array new ConstantIntegerType(1), new ConstantIntegerType(2), ]), - 'non-empty-array>', + 'non-empty-array>', ], [ new UnionType([ @@ -225,15 +228,13 @@ public function dataGeneralize(): array ]; } - /** - * @dataProvider dataGeneralize - */ + #[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, $a); - $scopeB = $scopeFactory->create(ScopeContext::create('file.php'))->assignVariable('a', $b, $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())); } @@ -248,4 +249,26 @@ public function testGetConstantType(): void $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 fb3a9dd5b1..680f528815 100644 --- a/tests/PHPStan/Analyser/StatementResultTest.php +++ b/tests/PHPStan/Analyser/StatementResultTest.php @@ -5,16 +5,18 @@ 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 PHPStanTestCase { - public function dataIsAlwaysTerminating(): array + public static function dataIsAlwaysTerminating(): array { return [ [ @@ -173,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, @@ -231,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; }', @@ -376,9 +510,7 @@ public function dataIsAlwaysTerminating(): array ]; } - /** - * @dataProvider dataIsAlwaysTerminating - */ + #[DataProvider('dataIsAlwaysTerminating')] public function testIsAlwaysTerminating( string $code, bool $expectedIsAlwaysTerminating, @@ -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(), new StringType()) - ->assignVariable('x', new IntegerType(), new IntegerType()) - ->assignVariable('cond', new MixedType(), new MixedType()) - ->assignVariable('arr', new ArrayType(new MixedType(), new MixedType()), 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/ThrowsTagFromNativeFunctionStubTest.php b/tests/PHPStan/Analyser/ThrowsTagFromNativeFunctionStubTest.php index a6f515560b..a57c784dd9 100644 --- a/tests/PHPStan/Analyser/ThrowsTagFromNativeFunctionStubTest.php +++ b/tests/PHPStan/Analyser/ThrowsTagFromNativeFunctionStubTest.php @@ -3,19 +3,20 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class ThrowsTagFromNativeFunctionStubTest extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/throws-tag-from-native-function-stub.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/throws-tag-from-native-function-stub.php'); } /** - * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/TraitStubFilesTest.php b/tests/PHPStan/Analyser/TraitStubFilesTest.php index 9107f5f45c..097279e6d3 100644 --- a/tests/PHPStan/Analyser/TraitStubFilesTest.php +++ b/tests/PHPStan/Analyser/TraitStubFilesTest.php @@ -3,19 +3,20 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class TraitStubFilesTest extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/trait-stubs.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/trait-stubs.php'); } /** - * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/TypeSpecifierContextTest.php b/tests/PHPStan/Analyser/TypeSpecifierContextTest.php index c6e083ed5d..10c28510a5 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierContextTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierContextTest.php @@ -4,11 +4,12 @@ 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 [ [ @@ -35,9 +36,9 @@ public function dataContext(): array } /** - * @dataProvider dataContext * @param bool[] $results */ + #[DataProvider('dataContext')] public function testContext(TypeSpecifierContext $context, array $results): void { $this->assertSame($results[0], $context->true()); @@ -47,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 [ [ @@ -70,9 +71,9 @@ public function dataNegate(): array } /** - * @dataProvider dataNegate * @param bool[] $results */ + #[DataProvider('dataNegate')] public function testNegate(TypeSpecifierContext $context, array $results): void { $this->assertSame($results[0], $context->true()); diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index c2b21e92c4..5a06bfb0aa 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser; +use Override; use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Equal; @@ -21,6 +22,7 @@ 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; @@ -34,6 +36,7 @@ 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; @@ -44,7 +47,7 @@ class TypeSpecifierTest extends PHPStanTestCase { private const FALSEY_TYPE_DESCRIPTION = '0|0.0|\'\'|\'0\'|array{}|false|null'; - private const TRUTHY_TYPE_DESCRIPTION = 'mixed~' . self::FALSEY_TYPE_DESCRIPTION; + private const TRUTHY_TYPE_DESCRIPTION = 'mixed~(' . self::FALSEY_TYPE_DESCRIPTION . ')'; private const SURE_NOT_FALSEY = '~' . self::FALSEY_TYPE_DESCRIPTION; private const SURE_NOT_TRUTHY = '~' . self::TRUTHY_TYPE_DESCRIPTION; @@ -55,34 +58,35 @@ class TypeSpecifierTest extends PHPStanTestCase private Scope $scope; + #[Override] protected function setUp(): void { - $reflectionProvider = $this->createReflectionProvider(); + $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'), new ObjectType('Bar')); - $this->scope = $this->scope->assignVariable('stringOrNull', new UnionType([new StringType(), new NullType()]), new UnionType([new StringType(), new NullType()])); - $this->scope = $this->scope->assignVariable('string', new StringType(), new StringType()); - $this->scope = $this->scope->assignVariable('fooOrNull', new UnionType([new ObjectType('Foo'), new NullType()]), new UnionType([new ObjectType('Foo'), new NullType()])); - $this->scope = $this->scope->assignVariable('barOrNull', new UnionType([new ObjectType('Bar'), new NullType()]), new UnionType([new ObjectType('Bar'), new NullType()])); - $this->scope = $this->scope->assignVariable('barOrFalse', new UnionType([new ObjectType('Bar'), new ConstantBooleanType(false)]), new UnionType([new ObjectType('Bar'), new ConstantBooleanType(false)])); - $this->scope = $this->scope->assignVariable('stringOrFalse', new UnionType([new StringType(), new ConstantBooleanType(false)]), new UnionType([new StringType(), new ConstantBooleanType(false)])); - $this->scope = $this->scope->assignVariable('array', new ArrayType(new MixedType(), new MixedType()), new ArrayType(new MixedType(), new MixedType())); - $this->scope = $this->scope->assignVariable('foo', new MixedType(), new MixedType()); - $this->scope = $this->scope->assignVariable('classString', new ClassStringType(), new ClassStringType()); - $this->scope = $this->scope->assignVariable('genericClassString', new GenericClassStringType(new ObjectType('Bar')), new GenericClassStringType(new ObjectType('Bar'))); - $this->scope = $this->scope->assignVariable('object', new ObjectWithoutClassType(), new ObjectWithoutClassType()); - $this->scope = $this->scope->assignVariable('int', new IntegerType(), new IntegerType()); - $this->scope = $this->scope->assignVariable('float', new FloatType(), new FloatType()); + $this->scope = $this->scope->assignVariable('bar', new ObjectType('Bar'), new ObjectType('Bar'), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('stringOrNull', new UnionType([new StringType(), new NullType()]), new UnionType([new StringType(), new NullType()]), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('string', new StringType(), new StringType(), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('fooOrNull', new UnionType([new ObjectType('Foo'), new NullType()]), new UnionType([new ObjectType('Foo'), new NullType()]), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('barOrNull', new UnionType([new ObjectType('Bar'), new NullType()]), new UnionType([new ObjectType('Bar'), new NullType()]), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('barOrFalse', new UnionType([new ObjectType('Bar'), new ConstantBooleanType(false)]), new UnionType([new ObjectType('Bar'), new ConstantBooleanType(false)]), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('stringOrFalse', new UnionType([new StringType(), new ConstantBooleanType(false)]), new UnionType([new StringType(), new ConstantBooleanType(false)]), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('array', new ArrayType(new MixedType(), new MixedType()), new ArrayType(new MixedType(), new MixedType()), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('foo', new MixedType(), new MixedType(), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('classString', new ClassStringType(), new ClassStringType(), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('genericClassString', new GenericClassStringType(new ObjectType('Bar')), new GenericClassStringType(new ObjectType('Bar')), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('object', new ObjectWithoutClassType(), new ObjectWithoutClassType(), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('int', new IntegerType(), new IntegerType(), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('float', new FloatType(), new FloatType(), TrinaryLogic::createYes()); } /** - * @dataProvider dataCondition * @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()); @@ -94,7 +98,7 @@ public function testCondition(Expr $expr, array $expectedPositiveResult, array $ $this->assertSame($expectedNegatedResult, $actualResult, sprintf('if not (%s)', $this->printer->prettyPrintExpr($expr))); } - public function dataCondition(): iterable + public static function dataCondition(): iterable { if (PHP_VERSION_ID >= 80100) { yield [ @@ -130,86 +134,86 @@ public function dataCondition(): iterable } yield from [ [ - $this->createFunctionCall('is_int'), + self::createFunctionCall('is_int'), ['$foo' => 'int'], ['$foo' => '~int'], ], [ - $this->createFunctionCall('is_numeric'), + 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'], ], @@ -279,7 +283,7 @@ public function dataCondition(): iterable [ new Expr\BinaryOp\BooleanAnd( new Variable('foo'), - $this->createFunctionCall('random'), + self::createFunctionCall('random'), ), ['$foo' => self::SURE_NOT_FALSEY], [], @@ -287,7 +291,7 @@ public function dataCondition(): iterable [ new Expr\BinaryOp\BooleanOr( new Variable('foo'), - $this->createFunctionCall('random'), + self::createFunctionCall('random'), ), [], ['$foo' => self::SURE_NOT_TRUTHY], @@ -306,7 +310,7 @@ public function dataCondition(): iterable [ new Expr\BinaryOp\BooleanAnd( new PropertyFetch(new Variable('this'), 'foo'), - $this->createFunctionCall('random'), + self::createFunctionCall('random'), ), ['$this->foo' => self::SURE_NOT_FALSEY], [], @@ -314,7 +318,7 @@ public function dataCondition(): iterable [ new Expr\BinaryOp\BooleanOr( new PropertyFetch(new Variable('this'), 'foo'), - $this->createFunctionCall('random'), + self::createFunctionCall('random'), ), [], ['$this->foo' => self::SURE_NOT_TRUTHY], @@ -327,18 +331,18 @@ public function dataCondition(): iterable [ 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'], @@ -346,8 +350,8 @@ public function dataCondition(): iterable ], [ 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'], @@ -355,10 +359,10 @@ public function dataCondition(): iterable [ 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'], [], @@ -366,10 +370,10 @@ public function dataCondition(): iterable [ 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' => 'mixed'], @@ -377,10 +381,10 @@ public function dataCondition(): iterable [ 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'), ), [], [], @@ -388,10 +392,10 @@ public function dataCondition(): iterable [ 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'], @@ -399,10 +403,10 @@ public function dataCondition(): iterable [ 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' => 'mixed'], [], @@ -426,7 +430,7 @@ public function dataCondition(): iterable ], [ new Identical( - $this->createFunctionCall('is_int'), + self::createFunctionCall('is_int'), new Expr\ConstFetch(new Name('true')), ), ['is_int($foo)' => 'true', '$foo' => 'int'], @@ -434,7 +438,7 @@ public function dataCondition(): iterable ], [ new Identical( - $this->createFunctionCall('is_string'), + self::createFunctionCall('is_string'), new Expr\ConstFetch(new Name('true')), ), ['is_string($foo)' => 'true', '$foo' => 'string'], @@ -442,7 +446,7 @@ public function dataCondition(): iterable ], [ new Identical( - $this->createFunctionCall('is_int'), + self::createFunctionCall('is_int'), new Expr\ConstFetch(new Name('false')), ), ['is_int($foo)' => 'false', '$foo' => '~int'], @@ -450,7 +454,7 @@ public function dataCondition(): iterable ], [ new Equal( - $this->createFunctionCall('is_int'), + self::createFunctionCall('is_int'), new Expr\ConstFetch(new Name('true')), ), ['$foo' => 'int'], @@ -458,7 +462,7 @@ public function dataCondition(): iterable ], [ new Equal( - $this->createFunctionCall('is_int'), + self::createFunctionCall('is_int'), new Expr\ConstFetch(new Name('false')), ), ['$foo' => '~int'], @@ -477,8 +481,8 @@ public function dataCondition(): iterable new Variable('foo'), new Expr\ConstFetch(new Name('null')), ), - ['$foo' => self::SURE_NOT_TRUTHY], - ['$foo' => self::SURE_NOT_FALSEY], + ['$foo' => '0|0.0|\'\'|array{}|false|null'], + ['$foo' => '~0|0.0|\'\'|array{}|false|null'], ], [ new Expr\BinaryOp\Identical( @@ -819,10 +823,10 @@ public function dataCondition(): iterable new LNumber(3), ), [ - '$n' => 'mixed~int<3, max>|true', + '$n' => 'mixed~(int<3, max>|true)', ], [ - '$n' => 'mixed~0.0|int|false|null', + '$n' => 'mixed~(0.0|int|false|null)', ], ], [ @@ -831,10 +835,10 @@ public function dataCondition(): iterable new LNumber(PHP_INT_MIN), ), [ - '$n' => 'mixed~int<' . PHP_INT_MIN . ', max>|true', + '$n' => 'mixed~(int<' . PHP_INT_MIN . ', max>|true)', ], [ - '$n' => 'mixed~0.0|false|null', + '$n' => 'mixed~(0.0|false|null)', ], ], [ @@ -843,7 +847,7 @@ public function dataCondition(): iterable new LNumber(PHP_INT_MAX), ), [ - '$n' => 'mixed~0.0|bool|int|null', + '$n' => 'mixed~(0.0|bool|int|null)', ], [ '$n' => 'mixed', @@ -858,7 +862,7 @@ public function dataCondition(): iterable '$n' => 'mixed~int<' . (PHP_INT_MIN + 1) . ', max>', ], [ - '$n' => 'mixed~0.0|bool|int|null', + '$n' => 'mixed~(0.0|bool|int|null)', ], ], [ @@ -867,10 +871,10 @@ public function dataCondition(): iterable new LNumber(PHP_INT_MAX), ), [ - '$n' => 'mixed~0.0|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)', ], ], [ @@ -885,10 +889,10 @@ public function dataCondition(): iterable ), ), [ - '$n' => 'mixed~0.0|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)', ], ], [ @@ -1033,7 +1037,7 @@ public function dataCondition(): iterable ]), ), [ - '$array' => 'array', + '$array' => 'non-empty-array', ], [ '$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')', @@ -1054,7 +1058,7 @@ public function dataCondition(): iterable '$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')', ], [ - '$array' => 'array', + '$array' => 'non-empty-array', ], ], [ @@ -1063,7 +1067,7 @@ public function dataCondition(): iterable new Arg(new Variable('array')), ]), [ - '$array' => 'array&hasOffset(\'foo\')', + '$array' => 'non-empty-array&hasOffset(\'foo\')', ], [ '$array' => '~hasOffset(\'foo\')', @@ -1081,7 +1085,7 @@ public function dataCondition(): iterable ]), ), [ - '$array' => 'array', + '$array' => 'non-empty-array', ], [ '$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')', @@ -1102,7 +1106,7 @@ public function dataCondition(): iterable '$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')', ], [ - '$array' => 'array', + '$array' => 'non-empty-array', ], ], [ @@ -1111,7 +1115,7 @@ public function dataCondition(): iterable new Arg(new Variable('array')), ]), [ - '$array' => 'array&hasOffset(\'foo\')', + '$array' => 'non-empty-array&hasOffset(\'foo\')', ], [ '$array' => '~hasOffset(\'foo\')', @@ -1133,9 +1137,7 @@ public function dataCondition(): iterable new Arg(new Variable('stringOrNull')), new Arg(new Expr\ConstFetch(new Name('false'))), ]), - [ - '$object' => 'object', - ], + [], [], ], [ @@ -1184,7 +1186,7 @@ public function dataCondition(): iterable [ new Expr\BinaryOp\BooleanOr( new Expr\BinaryOp\BooleanAnd( - $this->createFunctionCall('is_string', 'a'), + self::createFunctionCall('is_string', 'a'), new NotIdentical(new String_(''), new Variable('a')), ), new Identical(new Expr\ConstFetch(new Name('null')), new Variable('a')), @@ -1195,9 +1197,9 @@ public function dataCondition(): iterable [ new Expr\BinaryOp\BooleanOr( new Expr\BinaryOp\BooleanAnd( - $this->createFunctionCall('is_string', 'a'), + self::createFunctionCall('is_string', 'a'), new Expr\BinaryOp\Greater( - $this->createFunctionCall('strlen', 'a'), + self::createFunctionCall('strlen', 'a'), new LNumber(0), ), ), @@ -1209,20 +1211,20 @@ public function dataCondition(): iterable [ new Expr\BinaryOp\BooleanOr( new Expr\BinaryOp\BooleanAnd( - $this->createFunctionCall('is_array', 'a'), + self::createFunctionCall('is_array', 'a'), new Expr\BinaryOp\Greater( - $this->createFunctionCall('count', 'a'), + 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'], + ['$a' => 'non-empty-array|null'], + ['$a' => 'mixed~non-empty-array & ~null'], ], [ new Expr\BinaryOp\BooleanAnd( - $this->createFunctionCall('is_array', 'foo'), + self::createFunctionCall('is_array', 'foo'), new Identical( new FuncCall( new Name('array_filter'), @@ -1233,13 +1235,13 @@ public function dataCondition(): iterable ), [ '$foo' => 'array', - 'array_filter($foo, \'is_string\', ARRAY_FILTER_USE_KEY)' => 'array', // could be 'array' + 'array_filter($foo, \'is_string\', ARRAY_FILTER_USE_KEY)' => 'array', // could be 'array' ], [], ], [ new Expr\BinaryOp\BooleanAnd( - $this->createFunctionCall('is_array', 'foo'), + self::createFunctionCall('is_array', 'foo'), new Expr\BinaryOp\GreaterOrEqual( new FuncCall( new Name('count'), @@ -1249,14 +1251,14 @@ public function dataCondition(): iterable ), ), [ - '$foo' => 'non-empty-array', - 'count($foo)' => 'mixed~0.0|int|false|null', + '$foo' => 'non-empty-array', + 'count($foo)' => 'mixed~(0.0|int|false|null)', ], [], ], [ new Expr\BinaryOp\BooleanAnd( - $this->createFunctionCall('is_array', 'foo'), + self::createFunctionCall('is_array', 'foo'), new Identical( new FuncCall( new Name('count'), @@ -1266,14 +1268,14 @@ public function dataCondition(): iterable ), ), [ - '$foo' => 'non-empty-array', + '$foo' => 'non-empty-array', 'count($foo)' => '2', ], [], ], [ new Expr\BinaryOp\BooleanAnd( - $this->createFunctionCall('is_string', 'foo'), + self::createFunctionCall('is_string', 'foo'), new NotIdentical( new FuncCall( new Name('strlen'), @@ -1283,7 +1285,7 @@ public function dataCondition(): iterable ), ), [ - '$foo' => 'non-empty-string', + '$foo' => "string & ~''", 'strlen($foo)' => '~0', ], [ @@ -1292,7 +1294,7 @@ public function dataCondition(): iterable ], [ new Expr\BinaryOp\BooleanAnd( - $this->createFunctionCall('is_numeric', 'int'), + self::createFunctionCall('is_numeric', 'int'), new Expr\BinaryOp\Equal( new Variable('int'), new Expr\Cast\Int_(new Variable('int')), @@ -1306,7 +1308,7 @@ public function dataCondition(): iterable ], [ new Expr\BinaryOp\BooleanAnd( - $this->createFunctionCall('is_numeric', 'float'), + self::createFunctionCall('is_numeric', 'float'), new Expr\BinaryOp\Equal( new Variable('float'), new Expr\Cast\Int_(new Variable('float')), @@ -1347,7 +1349,7 @@ private function toReadableResult(SpecifiedTypes $specifiedTypes): array /** * @param non-empty-string $className */ - private function createInstanceOf(string $className, string $variableName = 'foo'): Expr\Instanceof_ + private static function createInstanceOf(string $className, string $variableName = 'foo'): Expr\Instanceof_ { return new Expr\Instanceof_(new Variable($variableName), new Name($className)); } @@ -1355,7 +1357,7 @@ private function createInstanceOf(string $className, string $variableName = 'foo /** * @param non-empty-string $functionName */ - private function createFunctionCall(string $functionName, string $variableName = 'foo'): FuncCall + 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 3378922993..8b351b2a57 100644 --- a/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceFalseTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceFalseTest.php @@ -3,21 +3,22 @@ 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 mixed ...$args */ + #[DataProvider('dataTypeSpecifyingExtensionsFalse')] public function testTypeSpecifyingExtensionsFalse( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceNullTest.php b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceNullTest.php index bc8276fb59..de7d4a7e4e 100644 --- a/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceNullTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceNullTest.php @@ -3,21 +3,22 @@ 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 mixed ...$args */ + #[DataProvider('dataTypeSpecifyingExtensionsNull')] public function testTypeSpecifyingExtensionsNull( string $assertType, string $file, diff --git a/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceTrueTest.php b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceTrueTest.php index 7478a93f2a..25a9e8a0b2 100644 --- a/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceTrueTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceTrueTest.php @@ -3,21 +3,22 @@ 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 mixed ...$args */ + #[DataProvider('dataTypeSpecifyingExtensionsTrue')] public function testTypeSpecifyingExtensionsTrue( string $assertType, string $file, 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/data/MethodCallReturnsBoolExpressionTypeResolverExtension.php b/tests/PHPStan/Analyser/data/MethodCallReturnsBoolExpressionTypeResolverExtension.php index fa6fb0a43a..eb02e6e436 100644 --- a/tests/PHPStan/Analyser/data/MethodCallReturnsBoolExpressionTypeResolverExtension.php +++ b/tests/PHPStan/Analyser/data/MethodCallReturnsBoolExpressionTypeResolverExtension.php @@ -34,7 +34,11 @@ public function getType(Expr $expr, Scope $scope): ?Type return null; } - $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + $returnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->getArgs(), + $methodReflection->getVariants() + )->getReturnType(); if ($returnType instanceof StringType) { return null; diff --git a/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php b/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php index 446e478cd0..dd72c4525e 100644 --- a/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php +++ b/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php @@ -41,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); @@ -75,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()); @@ -105,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); @@ -215,7 +247,11 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); } } diff --git a/tests/PHPStan/Analyser/data/array-destructuring.php b/tests/PHPStan/Analyser/data/array-destructuring.php index abf9bff20c..69e8b03786 100644 --- a/tests/PHPStan/Analyser/data/array-destructuring.php +++ b/tests/PHPStan/Analyser/data/array-destructuring.php @@ -1,5 +1,5 @@ true, 'value' => '123']; diff --git a/tests/PHPStan/Analyser/data/array-spread.php b/tests/PHPStan/Analyser/data/array-spread.php index 5bd12fdc38..f752a03270 100644 --- a/tests/PHPStan/Analyser/data/array-spread.php +++ b/tests/PHPStan/Analyser/data/array-spread.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 += 7.4 +): 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-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 @@ += 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-4715.php b/tests/PHPStan/Analyser/data/bug-4715.php index d51a97b3e4..508320fb8b 100644 --- a/tests/PHPStan/Analyser/data/bug-4715.php +++ b/tests/PHPStan/Analyser/data/bug-4715.php @@ -30,7 +30,7 @@ class Administration {} class Company { /** - * @var Collection|Administration[] + * @var Collection */ protected Collection $administrations; @@ -40,7 +40,7 @@ public function __construct() } /** - * @return Collection|Administration[] + * @return Collection */ public function getAdministrations() : Collection { diff --git a/tests/PHPStan/Analyser/data/bug-4902.php b/tests/PHPStan/Analyser/data/bug-4902.php index 8e553747ac..fc84a47d33 100644 --- a/tests/PHPStan/Analyser/data/bug-4902.php +++ b/tests/PHPStan/Analyser/data/bug-4902.php @@ -1,4 +1,4 @@ -= 7.4 + 'Hi'); - echo say((fn (string $name = null) => 'Hi')(...)); + echo say(fn (?string $name = null) => 'Hi'); + echo say((fn (?string $name = null) => 'Hi')(...)); - echo say(function (string $name = null) { + echo say(function (?string $name = null) { return 'Hi'; }); - echo say((function (string $name = null) { + echo say((function (?string $name = null) { return 'Hi'; })(...)); }; 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 +get(0)); - assertType('bool', $this->get(1)); - assertType('bool', $this->get(2)); - } -} 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/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/functions.php b/tests/PHPStan/Analyser/data/functions.php index ff580ad6a4..01b6d33f3d 100644 --- a/tests/PHPStan/Analyser/data/functions.php +++ b/tests/PHPStan/Analyser/data/functions.php @@ -56,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(); diff --git a/tests/PHPStan/Analyser/data/ignore-next-line-legacy.php b/tests/PHPStan/Analyser/data/ignore-next-line-legacy.php deleted file mode 100644 index fa8f2a50fd..0000000000 --- a/tests/PHPStan/Analyser/data/ignore-next-line-legacy.php +++ /dev/null @@ -1,40 +0,0 @@ -|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-php72.php b/tests/PHPStan/Analyser/data/mb-strlen-php72.php deleted file mode 100644 index 86069cb774..0000000000 --- a/tests/PHPStan/Analyser/data/mb-strlen-php72.php +++ /dev/null @@ -1,56 +0,0 @@ -= 7.2 - -namespace MbStrlenPhp72; - -use function PHPStan\Testing\assertType; - -class MbStrlenPhp72 -{ - - /** - * @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('3', mb_strlen('foo', $encodingsValidOnlyUntilPhp72)); - } - -} - 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/param-out.php b/tests/PHPStan/Analyser/data/param-out.php index 1f4fc69bd3..e34e1c9082 100644 --- a/tests/PHPStan/Analyser/data/param-out.php +++ b/tests/PHPStan/Analyser/data/param-out.php @@ -240,7 +240,7 @@ function foo16() { function fooShuffle() { $array = ["foo" => 123, "bar" => 456]; shuffle($array); - assertType('non-empty-array<0|1, 123|456>&list', $array); + assertType('non-empty-list<123|456>', $array); $emptyArray = []; shuffle($emptyArray); @@ -315,7 +315,7 @@ function testParseStr() { echo $output['arr'][1];//baz */ - \PHPStan\Testing\assertType('array', $output); + \PHPStan\Testing\assertType('array|lowercase-string>', $output); } function fooSimilar() { @@ -392,10 +392,10 @@ function fooHeadersSent() { function fooMbParseStr() { mb_parse_str("foo=bar", $output); - assertType('array', $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', $output); + assertType('array|lowercase-string>', $output); } function fooPreg() @@ -501,4 +501,3 @@ function testMatch() { preg_match('#.*#', 'foo', $matches); assertType('array{0?: string}', $matches); } - 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 @@ -= 7.4 + assertType('int', $nullable), + self::ALLOW_NULLABLE_INT => assertType('int|null', $nullable), + }; + } +} + + + diff --git a/tests/PHPStan/Analyser/data/str-split-php74.php b/tests/PHPStan/Analyser/data/str-split-php74.php new file mode 100644 index 0000000000..da1c955fb0 --- /dev/null +++ b/tests/PHPStan/Analyser/data/str-split-php74.php @@ -0,0 +1,40 @@ +|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/unknown-mixed-type.php b/tests/PHPStan/Analyser/data/unknown-mixed-type.php new file mode 100644 index 0000000000..d603b6d3ef --- /dev/null +++ b/tests/PHPStan/Analyser/data/unknown-mixed-type.php @@ -0,0 +1,13 @@ += 8.0 + +declare(strict_types = 1); namespace Abs; 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/array-change-key-case.php b/tests/PHPStan/Analyser/nsrt/array-change-key-case.php new file mode 100644 index 0000000000..aef6c0ac75 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-change-key-case.php @@ -0,0 +1,98 @@ + $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.php b/tests/PHPStan/Analyser/nsrt/array-chunk.php index a1452bd6c3..cedb50ddb7 100644 --- a/tests/PHPStan/Analyser/nsrt/array-chunk.php +++ b/tests/PHPStan/Analyser/nsrt/array-chunk.php @@ -11,7 +11,7 @@ public function generalArrays(array $arr): void { /** @var mixed[] $arr */ assertType('list>', array_chunk($arr, 2)); - assertType('list', array_chunk($arr, 2, true)); + assertType('list>', array_chunk($arr, 2, true)); /** @var array $arr */ assertType('list>', array_chunk($arr, 2)); @@ -60,8 +60,8 @@ public function chunkUnionTypeLength(array $arr, $positiveRange, $positiveUnion) * @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)); + assertType('list', array_chunk($arr, $positiveInt)); + assertType('list', array_chunk($arr, $bigger50)); } /** @@ -74,5 +74,26 @@ function testLimits(array $arr, int $oneToFour, int $tooBig) { 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-php82.php b/tests/PHPStan/Analyser/nsrt/array-column-php82.php index 62350f5992..e55e7a38ba 100644 --- a/tests/PHPStan/Analyser/nsrt/array-column-php82.php +++ b/tests/PHPStan/Analyser/nsrt/array-column-php82.php @@ -13,6 +13,7 @@ class ArrayColumnTest 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')); } @@ -22,12 +23,14 @@ 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')); } @@ -66,6 +69,7 @@ public function testArray8(array $array): void 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')); } @@ -74,6 +78,7 @@ public function testConstantArray1(array $array): void 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')); } @@ -81,6 +86,7 @@ public function testConstantArray2(array $array): void 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')); } @@ -96,6 +102,7 @@ public function testConstantArray4(array $array): void 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')); } @@ -104,12 +111,14 @@ public function testConstantArray5(array $array): void 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')); } @@ -142,6 +151,7 @@ public function testConstantArray11(array $array): void 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')); } @@ -151,6 +161,7 @@ public function testConstantArray12(array $array): void 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')); } @@ -166,6 +177,7 @@ public function testImprecise2(array $array): void 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')); } @@ -173,36 +185,42 @@ public function testImprecise3(array $array): void 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')); + 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')); + 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')); + 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')); + 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')); + 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')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); } } @@ -214,6 +232,7 @@ final class Foo 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 index 2455d6ace9..4f830b96d9 100644 --- a/tests/PHPStan/Analyser/nsrt/array-column.php +++ b/tests/PHPStan/Analyser/nsrt/array-column.php @@ -13,6 +13,7 @@ class ArrayColumnTest 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')); } @@ -22,12 +23,14 @@ 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')); } @@ -66,6 +69,7 @@ public function testArray8(array $array): void 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')); } @@ -74,6 +78,7 @@ public function testConstantArray1(array $array): void 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')); } @@ -81,6 +86,7 @@ public function testConstantArray2(array $array): void 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')); } @@ -96,6 +102,7 @@ public function testConstantArray4(array $array): void 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')); } @@ -104,12 +111,14 @@ public function testConstantArray5(array $array): void 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')); } @@ -142,6 +151,7 @@ public function testConstantArray11(array $array): void 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')); } @@ -149,6 +159,7 @@ public function testConstantArray12(array $array): void 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')); } @@ -156,6 +167,7 @@ public function testConstantArray13(array $array): void 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')); } @@ -165,6 +177,7 @@ public function testConstantArray14(array $array): void 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')); } @@ -180,6 +193,7 @@ public function testImprecise2(array $array): void 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')); } @@ -187,36 +201,42 @@ public function testImprecise3(array $array): void 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')); + 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')); + 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')); + 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')); + 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')); + 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')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); } } @@ -227,8 +247,8 @@ 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')); + 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 index 7e589c5f6e..7982233b61 100644 --- a/tests/PHPStan/Analyser/nsrt/array-combine-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array-combine-php7.php @@ -33,7 +33,7 @@ function withBoolKey(): void $c = [false, 'red', 'yellow']; $d = ['avocado', 'apple', 'banana']; - assertType("array{: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($c, $d)); + assertType("array{'': 'avocado', red: 'apple', yellow: 'banana'}", array_combine($c, $d)); } function withFloatKey(): void @@ -41,7 +41,7 @@ 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)); + assertType("array{'1.5': 'avocado', red: 'apple', yellow: 'banana'}", array_combine($a, $b)); } function withIntegerKey(): void diff --git a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php index 073fb23770..0f415089bd 100644 --- a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php @@ -33,7 +33,7 @@ function withBoolKey(): void $c = [false, 'red', 'yellow']; $d = ['avocado', 'apple', 'banana']; - assertType("array{: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($c, $d)); + assertType("array{'': 'avocado', red: 'apple', yellow: 'banana'}", array_combine($c, $d)); } function withFloatKey(): void @@ -41,7 +41,7 @@ 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)); + assertType("array{'1.5': 'avocado', red: 'apple', yellow: 'banana'}", array_combine($a, $b)); } function withIntegerKey(): void @@ -83,3 +83,11 @@ 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-fill-keys-php7.php b/tests/PHPStan/Analyser/nsrt/array-fill-keys-php7.php index 7bbc485b5a..c0a4e8dd7a 100644 --- a/tests/PHPStan/Analyser/nsrt/array-fill-keys-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array-fill-keys-php7.php @@ -7,7 +7,7 @@ function mixedAndSubtractedArray($mixed): void { if (is_array($mixed)) { - assertType("array<'b'>", array_fill_keys($mixed, 'b')); + assertType("array", 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 index 07c06367dc..4710482534 100644 --- a/tests/PHPStan/Analyser/nsrt/array-fill-keys-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-fill-keys-php8.php @@ -7,7 +7,7 @@ function mixedAndSubtractedArray($mixed): void { if (is_array($mixed)) { - assertType("array<'b'>", array_fill_keys($mixed, 'b')); + 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 index 5054186320..9a56ef0cfb 100644 --- a/tests/PHPStan/Analyser/nsrt/array-fill-keys.php +++ b/tests/PHPStan/Analyser/nsrt/array-fill-keys.php @@ -26,12 +26,12 @@ class Baz {} function withBoolKey() : array { assertType("array{1: 'b'}", array_fill_keys([true], 'b')); - assertType("array{: 'b'}", array_fill_keys([false], 'b')); + assertType("array{'': 'b'}", array_fill_keys([false], 'b')); } function withFloatKey() : array { - assertType("array{1.5: 'b'}", array_fill_keys([1.5], 'b')); + assertType("array{'1.5': 'b'}", array_fill_keys([1.5], 'b')); } function withIntegerKey() : array @@ -91,7 +91,7 @@ function withNotConstantArray(array $foo, array $bar, array $baz, array $floats, 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($floats, null)); assertType("array", array_fill_keys($mixed, null)); assertType('array', array_fill_keys($list, null)); assertType('*ERROR*', array_fill_keys($objectsWithoutToString, null)); diff --git a/tests/PHPStan/Analyser/nsrt/array-filter-arrow-functions.php b/tests/PHPStan/Analyser/nsrt/array-filter-arrow-functions.php index 8b3b17f477..4ad761fc71 100644 --- a/tests/PHPStan/Analyser/nsrt/array-filter-arrow-functions.php +++ b/tests/PHPStan/Analyser/nsrt/array-filter-arrow-functions.php @@ -1,4 +1,4 @@ -= 7.4 += 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 index ef684865fd..f6e0b6c65e 100644 --- a/tests/PHPStan/Analyser/nsrt/array-filter-string-callables.php +++ b/tests/PHPStan/Analyser/nsrt/array-filter-string-callables.php @@ -77,3 +77,14 @@ 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-php7.php b/tests/PHPStan/Analyser/nsrt/array-flip-php7.php index 0b7058de01..be0a3aa6f4 100644 --- a/tests/PHPStan/Analyser/nsrt/array-flip-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array-flip-php7.php @@ -7,9 +7,9 @@ function mixedAndSubtractedArray($mixed) { if (is_array($mixed)) { - assertType('array', array_flip($mixed)); + assertType('array<(int|string)>', array_flip($mixed)); } else { - assertType('mixed~array', $mixed); + 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 index b8f0e6793d..294554e20c 100644 --- a/tests/PHPStan/Analyser/nsrt/array-flip-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-flip-php8.php @@ -7,9 +7,9 @@ function mixedAndSubtractedArray($mixed) { if (is_array($mixed)) { - assertType('array', array_flip($mixed)); + assertType('array<(int|string)>', array_flip($mixed)); } else { - assertType('mixed~array', $mixed); + 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 index 2f02f1e733..9ec89f5c1d 100644 --- a/tests/PHPStan/Analyser/nsrt/array-flip.php +++ b/tests/PHPStan/Analyser/nsrt/array-flip.php @@ -20,7 +20,7 @@ function foo3($list) { $flip = array_flip($list); - assertType('array', $flip); + assertType('array<(int|string)>', $flip); } /** @@ -71,25 +71,25 @@ function foo8($mixed) function foo10(array $array) { if (array_key_exists('foo', $array)) { - assertType('array&hasOffset(\'foo\')', $array); - assertType('array', array_flip($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("array&hasOffsetValue('foo', int)", $array); - assertType('array', array_flip($array)); + 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("array&hasOffsetValue('foo', 17)", $array); - assertType("array&hasOffsetValue(17, 'foo')", array_flip($array)); + 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("array&hasOffsetValue('bar', 17)&hasOffsetValue('foo', 17)", $array); + 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-php7.php b/tests/PHPStan/Analyser/nsrt/array-intersect-key-php7.php index 79b7af7b18..14dd7b5d0d 100644 --- a/tests/PHPStan/Analyser/nsrt/array-intersect-key-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array-intersect-key-php7.php @@ -16,7 +16,7 @@ public function mixedAndSubtractedArray($mixed, array $otherArrs): void /** @var array $otherArrs */ assertType('array', array_intersect_key($mixed, $otherArrs)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); /** @var array $otherArrs */ assertType('null', array_intersect_key($mixed, $otherArrs)); /** @var array $otherArrs */ diff --git a/tests/PHPStan/Analyser/nsrt/array-intersect-key-php8.php b/tests/PHPStan/Analyser/nsrt/array-intersect-key-php8.php index 4bfd3140e0..9ffde9da83 100644 --- a/tests/PHPStan/Analyser/nsrt/array-intersect-key-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-intersect-key-php8.php @@ -16,7 +16,7 @@ public function mixedAndSubtractedArray($mixed, array $otherArrs): void /** @var array $otherArrs */ assertType('array', array_intersect_key($mixed, $otherArrs)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); /** @var array $otherArrs */ assertType('*NEVER*', array_intersect_key($mixed, $otherArrs)); /** @var array $otherArrs */ diff --git a/tests/PHPStan/Analyser/nsrt/array-intersect-key.php b/tests/PHPStan/Analyser/nsrt/array-intersect-key.php index bf620b508b..3369063444 100644 --- a/tests/PHPStan/Analyser/nsrt/array-intersect-key.php +++ b/tests/PHPStan/Analyser/nsrt/array-intersect-key.php @@ -45,10 +45,10 @@ public function normalArrays(array $arr, array $arr2, array $otherArrs): void /** @var array<17, int> $otherArrs */ assertType('array<17, string>', array_intersect_key($arr, $otherArrs)); /** @var array $otherArrs */ - assertType('array{}', array_intersect_key($arr, $otherArrs)); + assertType('array<\'\', string>', array_intersect_key($arr, $otherArrs)); if (array_key_exists(17, $arr2)) { - assertType('array<17, string>&hasOffset(17)', array_intersect_key($arr2, [17 => 'bar'])); + 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 */ @@ -56,7 +56,7 @@ public function normalArrays(array $arr, array $arr2, array $otherArrs): void } if (array_key_exists(17, $arr2) && $arr2[17] === 'foo') { - assertType("array<17, string>&hasOffsetValue(17, 'foo')", array_intersect_key($arr2, [17 => 'bar'])); + 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 */ @@ -79,7 +79,7 @@ public function arrayUnpacking(array $arrs, array $arrs2): void /** @param list $arr */ public function list(array $arr, array $otherArrs): void { - assertType('array<0|1, string>&list', array_intersect_key($arr, ['foo', 'bar'])); + assertType('list', array_intersect_key($arr, ['foo', 'bar'])); /** @var array $otherArrs */ assertType('array, string>', array_intersect_key($arr, $otherArrs)); /** @var array $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 index aa21248ec6..1b788158d6 100644 --- a/tests/PHPStan/Analyser/nsrt/array-is-list-type-specifying.php +++ b/tests/PHPStan/Analyser/nsrt/array-is-list-type-specifying.php @@ -6,7 +6,7 @@ function foo(array $foo) { if (array_is_list($foo)) { - assertType('list', $foo); + assertType('list', $foo); } else { assertType('array', $foo); } @@ -14,9 +14,9 @@ function foo(array $foo) { function foo2($foo) { if (array_is_list($foo)) { - assertType('list', $foo); + assertType('list', $foo); } else { - assertType('mixed~list', $foo); + assertType('mixed~list', $foo); } } @@ -49,7 +49,7 @@ function foo4(array $foo) { /** @var array $foo */ if (array_is_list($foo)) { - assertType('list', $foo); + assertType('list', $foo); } else { assertType('array', $foo); } diff --git a/tests/PHPStan/Analyser/nsrt/array-key-exists.php b/tests/PHPStan/Analyser/nsrt/array-key-exists.php index ed6f552d15..3d0505a9b9 100644 --- a/tests/PHPStan/Analyser/nsrt/array-key-exists.php +++ b/tests/PHPStan/Analyser/nsrt/array-key-exists.php @@ -1,4 +1,4 @@ -= 8.0 namespace ArrayKeyExistsExtension; @@ -50,16 +50,16 @@ public function doBar(array $a, array $b, array $c, int $key1, string $key2, int assertType('int', $key1); } if (array_key_exists($key2, $a)) { - assertType('numeric-string', $key2); + assertType('lowercase-string&numeric-string&uppercase-string', $key2); } if (array_key_exists($key3, $a)) { - assertType('int|numeric-string', $key3); + assertType('int|(lowercase-string&numeric-string&uppercase-string)', $key3); } if (array_key_exists($key4, $a)) { - assertType('(int|numeric-string)', $key4); + assertType('(int|(lowercase-string&numeric-string&uppercase-string))', $key4); } if (array_key_exists($key5, $a)) { - assertType('int|numeric-string', $key5); + assertType('int|(lowercase-string&numeric-string&uppercase-string)', $key5); } if (array_key_exists($key1, $b)) { diff --git a/tests/PHPStan/Analyser/nsrt/array-map.php b/tests/PHPStan/Analyser/nsrt/array-map.php index 75a68ab490..5dbafb1390 100644 --- a/tests/PHPStan/Analyser/nsrt/array-map.php +++ b/tests/PHPStan/Analyser/nsrt/array-map.php @@ -72,3 +72,48 @@ static function(string $string): string { 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-merge2.php b/tests/PHPStan/Analyser/nsrt/array-merge2.php index a52f640ef2..f0d86e61b6 100644 --- a/tests/PHPStan/Analyser/nsrt/array-merge2.php +++ b/tests/PHPStan/Analyser/nsrt/array-merge2.php @@ -21,6 +21,10 @@ public function arrayMergeArrayShapes($array1, $array2): void assertType("array{foo: '1', bar: '2', lall2: '3', 0: '4', 1: '6', lall: '3', 2: '2', 3: '3'}", array_merge($array2, $array1)); 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: 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]])); } /** diff --git a/tests/PHPStan/Analyser/nsrt/array-pop.php b/tests/PHPStan/Analyser/nsrt/array-pop.php index 04494a96df..37a986b0ce 100644 --- a/tests/PHPStan/Analyser/nsrt/array-pop.php +++ b/tests/PHPStan/Analyser/nsrt/array-pop.php @@ -77,7 +77,7 @@ public function foo1($mixed): void if(is_array($mixed)) { assertType('mixed', array_pop($mixed)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); assertType('mixed', array_pop($mixed)); assertType('*ERROR*', $mixed); } diff --git a/tests/PHPStan/Analyser/nsrt/array-replace.php b/tests/PHPStan/Analyser/nsrt/array-replace.php index 436ce232fa..93990c4f5b 100644 --- a/tests/PHPStan/Analyser/nsrt/array-replace.php +++ b/tests/PHPStan/Analyser/nsrt/array-replace.php @@ -23,9 +23,9 @@ public function arrayReplace($array1, $array2): void */ public function arrayReplaceArrayShapes($array1, $array2): void { - assertType("non-empty-array<'bar'|'foo', '1'|'2'>", array_replace($array1)); - assertType("non-empty-array<'bar'|'foo', '1'|'2'>", array_replace([], $array1)); - assertType("non-empty-array<'bar'|'foo', '1'|'2'|'4'>", array_replace($array1, $array2)); + assertType("array{foo: '1', bar: '2'}", array_replace($array1)); + assertType("array{foo: '1', bar: '2'}", array_replace([], $array1)); + assertType("array{foo: '1', bar: '4'}", array_replace($array1, $array2)); } /** @@ -68,4 +68,38 @@ public function arrayReplaceUnionTypeArrayShapes($array1, $array2): void 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 index e5a205ac0a..86a3bb72cf 100644 --- a/tests/PHPStan/Analyser/nsrt/array-reverse.php +++ b/tests/PHPStan/Analyser/nsrt/array-reverse.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types = 1); namespace ArrayReverse; @@ -12,8 +14,8 @@ class Foo */ public function normalArrays(array $a, array $b): void { - assertType('array', array_reverse($a)); - assertType('array', array_reverse($a, true)); + assertType('array', array_reverse($a)); + assertType('array', array_reverse($a, true)); assertType('array', array_reverse($b)); assertType('array', array_reverse($b, true)); @@ -22,8 +24,9 @@ public function normalArrays(array $a, array $b): void /** * @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): void + public function constantArrays(array $a, array $b, array $c): void { assertType('array{}', array_reverse([])); assertType('array{}', array_reverse([], true)); @@ -45,5 +48,32 @@ public function constantArrays(array $a, array $b): void 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 index 2cd24b7c9d..5816daf659 100644 --- a/tests/PHPStan/Analyser/nsrt/array-search-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array-search-php7.php @@ -16,7 +16,7 @@ public function mixedAndSubtractedArray($mixed, string $string): void assertType('int|string|false', array_search('foo', $mixed)); assertType('int|string|false', array_search($string, $mixed, true)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $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 index c3430cc1b5..30b9527e10 100644 --- a/tests/PHPStan/Analyser/nsrt/array-search-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-search-php8.php @@ -16,7 +16,7 @@ public function mixedAndSubtractedArray($mixed, string $string): void assertType('int|string|false', array_search('foo', $mixed)); assertType('int|string|false', array_search($string, $mixed, true)); } else { - assertType('mixed~array', $mixed); + 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.php b/tests/PHPStan/Analyser/nsrt/array-search.php index f26f6b7536..680c56844a 100644 --- a/tests/PHPStan/Analyser/nsrt/array-search.php +++ b/tests/PHPStan/Analyser/nsrt/array-search.php @@ -29,8 +29,8 @@ public function normalArrays(array $arr, string $string): void } if (array_key_exists(17, $arr) && $arr[17] === 'foo') { - assertType('17', array_search('foo', $arr, true)); - assertType('int|false', array_search('foo', $arr)); + assertType('int', array_search('foo', $arr, true)); + assertType('int', array_search('foo', $arr)); assertType('int|false', array_search($string, $arr, true)); } } @@ -39,25 +39,33 @@ public function constantArrays(array $arr, string $string): void { /** @var array{'a', 'b', 'c'} $arr */ assertType('1', array_search('b', $arr, true)); - assertType('0|1|2|false', array_search('b', $arr)); + assertType('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('0|1|2|false', array_search('b', $arr)); + 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('0|1|2|false', array_search('b', $arr)); + assertType('1|2', array_search('b', $arr)); assertType('0|1|2|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 index 0eaa4471d2..10049a8317 100644 --- a/tests/PHPStan/Analyser/nsrt/array-shape-list-optional.php +++ b/tests/PHPStan/Analyser/nsrt/array-shape-list-optional.php @@ -9,16 +9,22 @@ class Foo /** * @param list{0: string, 1: int, 2?: string, 3?: string} $valid1 + * @param non-empty-list{0?: string, 1?: int, 2?: string, 3?: string} $valid2 + * @param non-empty-array{0?: string, 1?: int, 2?: string, 3?: string} $valid3 * @param list{0: string, 1: int, 2?: string, 4?: string} $invalid1 * @param list{0: string, 1: int, 2?: string, foo?: string} $invalid2 */ public function doFoo( $valid1, + $valid2, + $valid3, $invalid1, $invalid2 ): void { - assertType('array{0: string, 1: int, 2?: string, 3?: string}&list', $valid1); + assertType('list{0: string, 1: int, 2?: string, 3?: string}', $valid1); + assertType('non-empty-list{0?: string, 1?: int, 2?: string, 3?: string}', $valid2); + assertType('non-empty-array{0?: string, 1?: int, 2?: string, 3?: string}', $valid3); assertType('*NEVER*', $invalid1); assertType('*NEVER*', $invalid2); } diff --git a/tests/PHPStan/Analyser/nsrt/array-shapes-keys-strings.php b/tests/PHPStan/Analyser/nsrt/array-shapes-keys-strings.php index c757be702d..82f9e9a6d7 100644 --- a/tests/PHPStan/Analyser/nsrt/array-shapes-keys-strings.php +++ b/tests/PHPStan/Analyser/nsrt/array-shapes-keys-strings.php @@ -17,8 +17,8 @@ class Foo */ public function doFoo(array $slash, array $dollar): void { - assertType('array{namespace/key: string}', $slash); - assertType('array', $dollar); + 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 index eb227de3f6..2d8ef21d7e 100644 --- a/tests/PHPStan/Analyser/nsrt/array-shift.php +++ b/tests/PHPStan/Analyser/nsrt/array-shift.php @@ -77,7 +77,7 @@ public function foo1($mixed): void if(is_array($mixed)) { assertType('mixed', array_shift($mixed)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); assertType('mixed', array_shift($mixed)); assertType('*ERROR*', $mixed); } diff --git a/tests/PHPStan/Analyser/nsrt/array-slice.php b/tests/PHPStan/Analyser/nsrt/array-slice.php index b28c660786..caf08c8d65 100644 --- a/tests/PHPStan/Analyser/nsrt/array-slice.php +++ b/tests/PHPStan/Analyser/nsrt/array-slice.php @@ -15,7 +15,7 @@ class Foo public function nonEmpty(array $a, array $b, array $c): void { assertType('array', array_slice($a, 1)); - assertType('list', array_slice($b, 1)); + assertType('list', array_slice($b, 1)); assertType('array', array_slice($c, 1)); } @@ -24,18 +24,34 @@ public function nonEmpty(array $a, array $b, array $c): void */ public function fromMixed($arr): void { - assertType('array', array_slice($arr, 1, 2)); + assertType('array', array_slice($arr, 1, 2)); } public function normalArrays(array $arr): void { /** @var array $arr */ - assertType('array', array_slice($arr, 1, 2)); + 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 @@ -43,10 +59,19 @@ 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 @@ -89,4 +114,17 @@ public function constantArraysWithOptionalKeys(array $arr): void 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_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('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 index 7ea0a40ff5..6808bf36b3 100644 --- a/tests/PHPStan/Analyser/nsrt/array_keys.php +++ b/tests/PHPStan/Analyser/nsrt/array_keys.php @@ -11,7 +11,7 @@ public function sayHello($mixed): void if(is_array($mixed)) { assertType('list<(int|string)>', array_keys($mixed)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); assertType('*NEVER*', array_keys($mixed)); } } diff --git a/tests/PHPStan/Analyser/nsrt/array_map_multiple.php b/tests/PHPStan/Analyser/nsrt/array_map_multiple.php index ce73048a46..2918ebeb89 100644 --- a/tests/PHPStan/Analyser/nsrt/array_map_multiple.php +++ b/tests/PHPStan/Analyser/nsrt/array_map_multiple.php @@ -15,7 +15,7 @@ public function doFoo(int $i, string $s): void return rand(0, 1) ? $a : $b; }, ['foo' => $i], ['bar' => $s]); - assertType('non-empty-array', $result); + assertType('non-empty-list', $result); } /** @@ -26,11 +26,15 @@ public function arrayMapNull(array $array, array $other): void { assertType('array{}', array_map(null, [])); assertType('array{foo: true}', array_map(null, ['foo' => true])); - assertType('non-empty-array', array_map(null, [1, 2, 3], [4, 5, 6])); + assertType('non-empty-list', array_map(null, [1, 2, 3], [4, 5, 6])); assertType('non-empty-array', array_map(null, $array)); - assertType('non-empty-array', array_map(null, $array, $array)); - assertType('non-empty-array', array_map(null, $array, $other)); + 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 index 7075c0fb8b..92385d0786 100644 --- a/tests/PHPStan/Analyser/nsrt/array_splice.php +++ b/tests/PHPStan/Analyser/nsrt/array_splice.php @@ -20,42 +20,367 @@ final class Foo function insertViaArraySplice(array $arr): void { $brr = $arr; - array_splice($brr, 0, 0, 1); - assertType('array', $brr); + $extract = array_splice($brr, 0, 0, 1); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); $brr = $arr; - array_splice($brr, 0, 0, [1]); - assertType('array', $brr); + $extract = array_splice($brr, 0, 0, [1]); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); $brr = $arr; - array_splice($brr, 0, 0, ''); - assertType('array', $brr); + $extract = array_splice($brr, 0, 0, ''); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); $brr = $arr; - array_splice($brr, 0, 0, ['']); - assertType('array', $brr); + $extract = array_splice($brr, 0, 0, ['']); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); $brr = $arr; - array_splice($brr, 0, 0, null); + $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; - array_splice($brr, 0, 0, [null]); - assertType('array', $brr); + $extract = array_splice($brr, 0, 0, new Foo()); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); $brr = $arr; - array_splice($brr, 0, 0, new Foo()); - assertType('array', $brr); + $extract = array_splice($brr, 0, 0, [new \stdClass()]); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); $brr = $arr; - array_splice($brr, 0, 0, [new \stdClass()]); - assertType('array', $brr); + $extract = array_splice($brr, 0, 0, false); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); $brr = $arr; - array_splice($brr, 0, 0, false); - assertType('array', $brr); + $extract = array_splice($brr, 0, 0, [false]); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); $brr = $arr; - array_splice($brr, 0, 0, [false]); - assertType('array', $brr); + $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 index 10de823980..db378c95aa 100644 --- a/tests/PHPStan/Analyser/nsrt/array_values-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array_values-php7.php @@ -12,7 +12,7 @@ public function foo1($mixed): void if (is_array($mixed)) { assertType('list', array_values($mixed)); } else { - assertType('mixed~array', $mixed); + 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 index d32544f3e6..18074963a4 100644 --- a/tests/PHPStan/Analyser/nsrt/array_values.php +++ b/tests/PHPStan/Analyser/nsrt/array_values.php @@ -11,7 +11,7 @@ public function foo1($mixed): void if(is_array($mixed)) { assertType('list', array_values($mixed)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); assertType('*NEVER*', array_values($mixed)); } } @@ -37,4 +37,12 @@ public function constantArrayType(): void ); 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 index 3e1448e6ff..a508035d19 100644 --- a/tests/PHPStan/Analyser/nsrt/arrow-function-argument-type.php +++ b/tests/PHPStan/Analyser/nsrt/arrow-function-argument-type.php @@ -21,6 +21,8 @@ public function doFoo(int $integer, array $array, ?string $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/nsrt/arrow-function-types.php b/tests/PHPStan/Analyser/nsrt/arrow-function-types.php index 77cbe12cf8..acb8f74ee4 100644 --- a/tests/PHPStan/Analyser/nsrt/arrow-function-types.php +++ b/tests/PHPStan/Analyser/nsrt/arrow-function-types.php @@ -1,4 +1,4 @@ -= 7.4 += 8.0 namespace AssertConditional; diff --git a/tests/PHPStan/Analyser/nsrt/assert-docblock.php b/tests/PHPStan/Analyser/nsrt/assert-docblock.php index 1b094a1cd7..a1cbcc9b3e 100644 --- a/tests/PHPStan/Analyser/nsrt/assert-docblock.php +++ b/tests/PHPStan/Analyser/nsrt/assert-docblock.php @@ -1,4 +1,4 @@ -= 8.0 namespace AssertDocblock; @@ -39,7 +39,7 @@ function validateStringOrIntArray(array $arr) : bool { * @param mixed[] $arr * @phpstan-assert-if-true =string[] $arr * @phpstan-assert-if-false =int[] $arr - * @phpstan-assert-if-false =non-empty-array $arr + * @phpstan-assert-if-false =non-empty-array $arr */ function validateStringOrNonEmptyIntArray(array $arr) : bool { return false; @@ -56,7 +56,7 @@ function validateNotNull($value) : void {} * @param mixed[] $arr */ function takesArray(array $arr) : void { - assertType('array', $arr); + assertType('array', $arr); validateStringArray($arr); assertType('array', $arr); @@ -66,22 +66,22 @@ function takesArray(array $arr) : void { * @param mixed[] $arr */ function takesArrayIfTrue(array $arr) : void { - assertType('array', $arr); + assertType('array', $arr); if (validateStringArrayIfTrue($arr)) { assertType('array', $arr); } else { - assertType('array', $arr); + assertType('array', $arr); } } /** * @param mixed[] $arr */ function takesArrayIfTrue1(array $arr) : void { - assertType('array', $arr); + assertType('array', $arr); if (!validateStringArrayIfTrue($arr)) { - assertType('array', $arr); + assertType('array', $arr); } else { assertType('array', $arr); } @@ -91,12 +91,12 @@ function takesArrayIfTrue1(array $arr) : void { * @param mixed[] $arr */ function takesArrayIfFalse(array $arr) : void { - assertType('array', $arr); + assertType('array', $arr); if (!validateStringArrayIfFalse($arr)) { assertType('array', $arr); } else { - assertType('array', $arr); + assertType('array', $arr); } } @@ -104,10 +104,10 @@ function takesArrayIfFalse(array $arr) : void { * @param mixed[] $arr */ function takesArrayIfFalse1(array $arr) : void { - assertType('array', $arr); + assertType('array', $arr); if (validateStringArrayIfFalse($arr)) { - assertType('array', $arr); + assertType('array', $arr); } else { assertType('array', $arr); } @@ -117,7 +117,7 @@ function takesArrayIfFalse1(array $arr) : void { * @param mixed[] $arr */ function takesStringOrIntArray(array $arr) : void { - assertType('array', $arr); + assertType('array', $arr); if (validateStringOrIntArray($arr)) { assertType('array', $arr); @@ -130,7 +130,7 @@ function takesStringOrIntArray(array $arr) : void { * @param mixed[] $arr */ function takesStringOrNonEmptyIntArray(array $arr) : void { - assertType('array', $arr); + assertType('array', $arr); if (validateStringOrNonEmptyIntArray($arr)) { assertType('array', $arr); diff --git a/tests/PHPStan/Analyser/nsrt/assert-empty.php b/tests/PHPStan/Analyser/nsrt/assert-empty.php index 12176791a3..a74e4c1e35 100644 --- a/tests/PHPStan/Analyser/nsrt/assert-empty.php +++ b/tests/PHPStan/Analyser/nsrt/assert-empty.php @@ -1,4 +1,4 @@ -= 8.0 namespace AssertEmpty; @@ -25,5 +25,5 @@ function ($var) { function ($var) { assertNotEmpty($var); - assertType("mixed~0|0.0|''|'0'|array{}|false|null", $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 index b9b362172e..ffc9552321 100644 --- a/tests/PHPStan/Analyser/nsrt/assert-inheritance.php +++ b/tests/PHPStan/Analyser/nsrt/assert-inheritance.php @@ -1,4 +1,4 @@ -= 8.0 namespace AssertInheritance; diff --git a/tests/PHPStan/Analyser/nsrt/assert-intersected.php b/tests/PHPStan/Analyser/nsrt/assert-intersected.php index a39ffe1436..913f1e9034 100644 --- a/tests/PHPStan/Analyser/nsrt/assert-intersected.php +++ b/tests/PHPStan/Analyser/nsrt/assert-intersected.php @@ -1,4 +1,4 @@ -= 8.0 namespace AssertIntersected; @@ -26,5 +26,5 @@ public function assert(mixed $value): void; function intersection($assert, mixed $value): void { $assert->assert($value); - assertType('non-empty-list', $value); + assertType('non-empty-list', $value); } diff --git a/tests/PHPStan/Analyser/nsrt/assert-invariant.php b/tests/PHPStan/Analyser/nsrt/assert-invariant.php index b7368f06e9..4efe160b18 100644 --- a/tests/PHPStan/Analyser/nsrt/assert-invariant.php +++ b/tests/PHPStan/Analyser/nsrt/assert-invariant.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types = 1); namespace AssertInvariant; diff --git a/tests/PHPStan/Analyser/nsrt/assert-methods.php b/tests/PHPStan/Analyser/nsrt/assert-methods.php index f6609f3f8f..6c278c21ed 100644 --- a/tests/PHPStan/Analyser/nsrt/assert-methods.php +++ b/tests/PHPStan/Analyser/nsrt/assert-methods.php @@ -37,7 +37,7 @@ public function doBar($mixed) public function doBar2(array $objects) { self::doFoo($objects, stdClass::class); - assertType('array', $objects); + assertType('array', $objects); } /** @@ -47,7 +47,7 @@ public function doBar2(array $objects) public function doBar3(array $strings) { self::doFoo($strings, stdClass::class); - assertType('array>', $strings); + assertType('array>', $strings); } /** 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 @@ += 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/block-statement.php b/tests/PHPStan/Analyser/nsrt/block-statement.php new file mode 100644 index 0000000000..fc752007d8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/block-statement.php @@ -0,0 +1,22 @@ += 8.0 + +declare(strict_types = 1); namespace Bug10037; diff --git a/tests/PHPStan/Analyser/nsrt/bug-10131.php b/tests/PHPStan/Analyser/nsrt/bug-10131.php index d78066e232..11088d2e44 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10131.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10131.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug10131; diff --git a/tests/PHPStan/Analyser/nsrt/bug-10189.php b/tests/PHPStan/Analyser/nsrt/bug-10189.php index 6d3d0e8645..6cb97fa4de 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10189.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10189.php @@ -20,7 +20,7 @@ function file_managed_file_save_upload(): array { } assertType('non-empty-array|Bug10189\SomeInterface', $files); $files = array_filter($files); - assertType("array", $files); + assertType("array", $files); return empty($files) ? [] : [1,2]; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-1021.php b/tests/PHPStan/Analyser/nsrt/bug-1021.php index b085b9a781..37e2f7244f 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-1021.php +++ b/tests/PHPStan/Analyser/nsrt/bug-1021.php @@ -13,7 +13,7 @@ function foobar() { } } - assertType('array{0?: int<1, max>, 1?: 2|3, 2?: 3}', $x); + assertType('list<1|2|3>', $x); if ($x) { } diff --git a/tests/PHPStan/Analyser/nsrt/bug-10254.php b/tests/PHPStan/Analyser/nsrt/bug-10254.php index 3299015ca0..a16ed81f04 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10254.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10254.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug10254; 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 @@ += 8.0 namespace Bug10473; 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-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-10717.php b/tests/PHPStan/Analyser/nsrt/bug-10717.php new file mode 100644 index 0000000000..a775284247 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10717.php @@ -0,0 +1,1052 @@ + */ + 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 index 67acc5964d..c82d2298f2 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10721.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10721.php @@ -22,9 +22,9 @@ public function retrieve(?int $limit = 20): array assertType("array{'zib', 'zib 2', 'zeit im bild', 'soko', 'landkrimi', 'tatort'}", $list); shuffle($list); - assertType("non-empty-array<0|1|2|3|4|5, 'landkrimi'|'soko'|'tatort'|'zeit im bild'|'zib'|'zib 2'>&list", $list); + assertType("non-empty-list<'landkrimi'|'soko'|'tatort'|'zeit im bild'|'zib'|'zib 2'>", $list); - assertType("non-empty-array<0|1|2|3|4|5, 'landkrimi'|'soko'|'tatort'|'zeit im bild'|'zib'|'zib 2'>&list", array_slice($list, 0, max($limit, 1))); + 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)); } @@ -37,7 +37,7 @@ public function listVariants(): void assertType("array{2: 'zib', 4: 'zib 2'}", $arr); shuffle($arr); - assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", $arr); + assertType("non-empty-list<'zib'|'zib 2'>", $arr); $list = [ 'zib', @@ -46,37 +46,37 @@ public function listVariants(): void assertType("array{'zib', 'zib 2'}", $list); shuffle($list); - assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", $list); - - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1)); - assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0)); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1)); // could be non-empty-array - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2)); - - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1, 1)); - assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0, 1)); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1, 1)); // could be non-empty-array - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2, 1)); - - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1, 2)); - assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0, 2)); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1, 2)); // could be non-empty-array - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2, 2)); - - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1, 3)); - assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0, 3)); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1, 3)); // could be non-empty-array - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2, 3)); - - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1, 3, true)); - assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0, 3, true)); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1, 3, true)); // could be non-empty-array - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2, 3, true)); - - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1, 3, false)); - assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0, 3, false)); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1, 3, false)); // could be non-empty-array - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2, 3, false)); + 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)); } /** @@ -86,15 +86,15 @@ public function listVariants(): void public function arrayVariants(array $strings, $maybeZero): void { assertType("array", $strings); - assertType("array", array_slice($strings, 0)); - assertType("array", array_slice($strings, 1)); - assertType("array", array_slice($strings, $maybeZero)); + 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-array", array_slice($strings, 0)); - assertType("array", array_slice($strings, 1)); - assertType("array", array_slice($strings, $maybeZero)); + 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 index 69efb18635..8e9050ac4e 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10834.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10834.php @@ -2,6 +2,8 @@ declare(strict_types=1); +namespace Bug10834; + use function PHPStan\Testing\assertType; /** diff --git a/tests/PHPStan/Analyser/nsrt/bug-10863.php b/tests/PHPStan/Analyser/nsrt/bug-10863.php index ecad48736e..c88d354382 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10863.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10863.php @@ -12,7 +12,7 @@ class Foo */ public function doFoo($b): void { - assertType('non-falsy-string', '@' . $b); + assertType('lowercase-string&non-falsy-string&uppercase-string', '@' . $b); } /** @@ -20,7 +20,7 @@ public function doFoo($b): void */ public function doFoo2($b): void { - assertType('non-falsy-string', '@' . $b); + 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 index 469c8956bd..0878d2f302 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10893.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10893.php @@ -5,16 +5,24 @@ use function PHPStan\Testing\assertType; /** - * @param non-falsy-string $nonfalsy + * @param non-falsy-string&numeric-string $str */ -function hasMicroseconds(\DateTimeInterface $value, string $nonfalsy): bool +function hasMicroseconds(\DateTimeInterface $value, string $str): bool { - assertType('non-falsy-string', $value->format('u')); + assertType('non-falsy-string&numeric-string', $str); + assertType('int', (int)$str); + assertType('bool', (int)$str !== 0); + + assertType('non-falsy-string&numeric-string', $value->format('u')); assertType('int', (int)$value->format('u')); assertType('bool', (int)$value->format('u') !== 0); - assertType('non-falsy-string', $nonfalsy); - assertType('int', (int)$nonfalsy); - assertType('bool', (int)$nonfalsy !== 0); + + 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-10952b.php b/tests/PHPStan/Analyser/nsrt/bug-10952b.php index f8f70e07d0..02386aa4b7 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10952b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10952b.php @@ -37,5 +37,14 @@ public function test(): void 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-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-11201.php b/tests/PHPStan/Analyser/nsrt/bug-11201.php index 74a41fa235..87625f777f 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11201.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11201.php @@ -41,7 +41,7 @@ function returnsBool(): bool { assertType('string', $s); $s = sprintf("%s", implode(', ', array_map('intval', returnsArray()))); -assertType('string', $s); +assertType('lowercase-string&uppercase-string', $s); $s = sprintf('%2$s', 1234, returnsNonFalsyString()); assertType('non-falsy-string', $s); @@ -53,4 +53,4 @@ function returnsBool(): bool { assertType("' 1'", $s); $s = sprintf('%20s', returnsBool()); -assertType("non-falsy-string", $s); +assertType("' '|' 1'", $s); 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-php72.php b/tests/PHPStan/Analyser/nsrt/bug-11311-php72.php deleted file mode 100644 index 40e6d99d9f..0000000000 --- a/tests/PHPStan/Analyser/nsrt/bug-11311-php72.php +++ /dev/null @@ -1,30 +0,0 @@ -\d+)\.(?\d+)(?:\.(?\d+))?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{0: string, major: numeric-string, 1: numeric-string, minor: numeric-string, 2: numeric-string, patch?: numeric-string, 3?: numeric-string}', $matches); - } -} - -function doUnmatchedAsNull(string $s): void { - if (preg_match('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType("array{0: string, 1?: ''|'foo', 2?: ''|'bar', 3?: 'baz'}", $matches); - } - assertType("array{}|array{0: string, 1?: ''|'foo', 2?: ''|'bar', 3?: 'baz'}", $matches); -} - -// see https://3v4l.org/VeDob#veol -function unmatchedAsNullWithOptionalGroup(string $s): void { - if (preg_match('/Price: (£|€)?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType("array{0: string, 1?: non-empty-string}", $matches); - } else { - assertType('array{}', $matches); - } - assertType("array{}|array{0: string, 1?: non-empty-string}", $matches); -} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11311.php b/tests/PHPStan/Analyser/nsrt/bug-11311.php index 3a01594ded..96b810431d 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11311.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11311.php @@ -1,4 +1,4 @@ -= 7.4 +\d+)\.(?\d+)(?:\.(?\d+))?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{0: string, major: numeric-string, 1: numeric-string, minor: numeric-string, 2: numeric-string, patch: numeric-string|null, 3: numeric-string|null}', $matches); + 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); } } @@ -23,11 +23,11 @@ function doUnmatchedAsNull(string $s): void { 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{string, non-empty-string|null}', $matches); + assertType("array{non-falsy-string, '£'|'€'|null}", $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{string, non-empty-string|null}', $matches); + assertType("array{}|array{non-falsy-string, '£'|'€'|null}", $matches); } function bug11331a(string $url):void { @@ -37,7 +37,7 @@ function bug11331a(string $url):void { (?.+) )? (?.+)}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, 2: non-empty-string}', $matches); + 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); } } @@ -63,20 +63,20 @@ function bug11331c(string $url):void { ([^/]+?) (?:\.git|/)? $}x', $url, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{string, non-empty-string|null, non-empty-string|null, non-empty-string, non-empty-string}', $matches); + 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{string, '£'|null, '€'|null}", $matches); // could be tagged union + 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{string, '£'|null, '€'|null}", $matches); // could be tagged union + assertType("array{non-falsy-string, '£'|null, '€'|null}", $matches); // could be tagged union } } } @@ -85,101 +85,101 @@ 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{string, numeric-string, numeric-string|null}', $matches); + 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{string, non-falsy-string, numeric-string, numeric-string, non-empty-string}', $matches); + 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{string, non-falsy-string, numeric-string, non-falsy-string|null}', $matches); + 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{string, numeric-string, ''|numeric-string}", $matches); + 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: string, num: numeric-string, 1: numeric-string}', $matches); + 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{string, non-falsy-string&numeric-string}', $matches); + 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{string, non-falsy-string}', $matches); + 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{string, non-empty-string}', $matches); + 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{string, non-empty-string}', $matches); + 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{string, string}', $matches); + 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{string, non-empty-string|null}', $matches); + 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{string, numeric-string}', $matches); + 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{string, numeric-string}', $matches); + 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{string, non-empty-string}', $matches); + 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{string, non-falsy-string|null, 'b'|'d'|'E'|'e'|'F'|'f'|'G'|'g'|'H'|'h'|'o'|'s'|'u'|'X'|'x'}", $matches); + 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); } }; @@ -191,36 +191,36 @@ function (string $s): void { function (string $s): void { preg_match('/%a(\d*)/', $s, $matches, PREG_UNMATCHED_AS_NULL); - assertType("array{0?: string, 1?: ''|numeric-string|null}", $matches); // could be array{0?: string, 1?: ''|numeric-string} + 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("array{0?: string, 1?: ''|numeric-string|null}", $matches); // could be array{0?: string, 1?: ''|numeric-string} + 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{string, numeric-string|null, non-empty-string|null}", $matches); + assertType("array{non-empty-string, numeric-string|null, non-empty-string|null}", $matches); } else { assertType("array{}", $matches); } - assertType("array{}|array{string, numeric-string|null, non-empty-string|null}", $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{string|null, int<-1, max>}, array{numeric-string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}}", $matches); + 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{string, 'ux'|null, 'u'|null, 'vy'|null, 'v'|null}", $matches); + 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("array{0?: string, 1?: numeric-string|null, 2?: non-empty-string|null}", $matches); + assertType("list{0?: string, 1?: numeric-string|null, 2?: non-empty-string|null}", $matches); }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-11472.php b/tests/PHPStan/Analyser/nsrt/bug-11472.php new file mode 100644 index 0000000000..a6ee71f048 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11472.php @@ -0,0 +1,18 @@ += 8.1 + +namespace Bug11472; + +use function PHPStan\Testing\assertType; + +/** + * @phpstan-return ($maybeFoo is 'foo' ? true : false) + */ +function isFoo(mixed $maybeFoo): bool +{ + return $maybeFoo === 'foo'; +} + +function (): void { + assertType('true', isFoo('foo')); + assertType('true', isFoo(...)('foo')); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-11518-types.php b/tests/PHPStan/Analyser/nsrt/bug-11518-types.php index 4f66f4f0af..19d5aeb15f 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11518-types.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11518-types.php @@ -12,12 +12,12 @@ function blah(array $a): array { if (!array_key_exists('thing', $a)) { $a['thing'] = 'bla'; - assertType('hasOffsetValue(\'thing\', \'bla\')&non-empty-array', $a); + assertType('non-empty-array&hasOffsetValue(\'thing\', \'bla\')', $a); } else { - assertType('array&hasOffset(\'thing\')', $a); + assertType('non-empty-array&hasOffset(\'thing\')', $a); } - assertType('array&hasOffsetValue(\'thing\', mixed)', $a); + assertType('non-empty-array&hasOffsetValue(\'thing\', mixed)', $a); return $a; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11547.php b/tests/PHPStan/Analyser/nsrt/bug-11547.php new file mode 100644 index 0000000000..3acb253f49 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11547.php @@ -0,0 +1,62 @@ += 8.0 + +namespace Bug11561; + +use function PHPStan\Testing\assertType; +use DateTime; + +/** @param array{date: DateTime} $c */ +function main(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + assertType('array{date: DateTime, id: 1}', $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1}", $c); + $c['name'] = 'ruud'; + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); + return 'x'; + })(); + + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); +} + + +/** @param array{date: DateTime} $c */ +function main2(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + $c['name'] = 'staabm'; + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + $c['name'] = 'ruud'; + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); + return 'x'; + })(); + + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); +} + +/** @param array{date: DateTime} $c */ +function main3(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + $c['name'] = 'staabm'; + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + if (rand(0,1)) { + $c['name'] = 'ruud'; + } + assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c); + return 'x'; + })(); + + assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c); +} + +/** @param array{date: DateTime} $c */ +function main4(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + $c['name'] = 'staabm'; + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + if (rand(0,1)) { + $c['name'] = 'ruud'; + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); + return 'y'; + } + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + return 'x'; + })(); + + assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-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-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-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/nsrt/bug-1209.php b/tests/PHPStan/Analyser/nsrt/bug-1209.php index fff8d13dff..4b4ce770d1 100644 --- a/tests/PHPStan/Analyser/nsrt/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/nsrt/bug-12173.php b/tests/PHPStan/Analyser/nsrt/bug-12173.php new file mode 100644 index 0000000000..e92ce7da4e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12173.php @@ -0,0 +1,19 @@ += 7.4 + +namespace Bug12173; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function parse(string $string): void + { + $regex = '#.*(?(apple|orange)).*#'; + + if (preg_match($regex, $string, $matches) !== 1) { + throw new \Exception('Invalid input'); + } + + assertType("'apple'|'orange'", $matches['fruit']);; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12182.php b/tests/PHPStan/Analyser/nsrt/bug-12182.php new file mode 100644 index 0000000000..5566a2a2da --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12182.php @@ -0,0 +1,19 @@ += 8.0 + +namespace Bug12182; + +use ArrayObject; +use function PHPStan\Testing\assertType; + +/** + * @extends ArrayObject + */ +class HelloWorld extends ArrayObject +{ + public function __construct(private int $a = 42) { + } +} + +function (HelloWorld $hw): void { + assertType('array', (array) $hw); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-12210.php b/tests/PHPStan/Analyser/nsrt/bug-12210.php new file mode 100644 index 0000000000..13cf62ed26 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12210.php @@ -0,0 +1,27 @@ += 7.4 + +declare(strict_types = 1); + +namespace Bug12210; + +use function PHPStan\Testing\assertType; + +function bug12210a(string $text): void { + assert(preg_match('(((sum|min|max)))', $text, $match) === 1); + assertType("array{non-empty-string, 'max'|'min'|'sum', 'max'|'min'|'sum'}", $match); +} + +function bug12210b(string $text): void { + assert(preg_match('(((sum|min|ma.)))', $text, $match) === 1); + assertType("array{non-empty-string, non-empty-string, non-falsy-string}", $match); +} + +function bug12210c(string $text): void { + assert(preg_match('(((su.|min|max)))', $text, $match) === 1); + assertType("array{non-empty-string, non-empty-string, non-falsy-string}", $match); +} + +function bug12210d(string $text): void { + assert(preg_match('(((sum|mi.|max)))', $text, $match) === 1); + assertType("array{non-empty-string, non-empty-string, non-falsy-string}", $match); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12211.php b/tests/PHPStan/Analyser/nsrt/bug-12211.php new file mode 100644 index 0000000000..33131edfe8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12211.php @@ -0,0 +1,16 @@ += 7.4 + +declare(strict_types = 1); + +namespace Bug12211; + +use function PHPStan\Testing\assertType; + +const REGEX = '((m.x))'; + +function foo(string $text): void { + assert(preg_match(REGEX, $text, $match) === 1); + assertType('array{non-falsy-string, non-falsy-string}', $match); +} + + diff --git a/tests/PHPStan/Analyser/nsrt/bug-12242.php b/tests/PHPStan/Analyser/nsrt/bug-12242.php new file mode 100644 index 0000000000..d9335610d3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12242.php @@ -0,0 +1,43 @@ += 7.4 + +namespace Bug12242; + +use function PHPStan\Testing\assertType; + +function foo(string $str): void +{ + $regexp = '/ + # ( + ([\d,]*) + # ) + /x'; + if (preg_match($regexp, $str, $match)) { + assertType('array{string, string}', $match); + } +} + +function bar(string $str): void +{ + $regexp = '/^ + (\w+) # column type [1] + [\(] # ( + ?([\d,]*) # size or size, precision [2] + [\)] # ) + ?\s* # whitespace + (\w*) # extra description (UNSIGNED, CHARACTER SET, ...) [3] + $/x'; + if (preg_match($regexp, $str, $matches)) { + assertType('array{non-falsy-string, non-empty-string, string, string}', $matches); + } +} + +function foobar(string $str): void +{ + $regexp = '/ + # ( + ([\d,]*)# a comment immediately behind with a closing parenthesis ) + /x'; + if (preg_match($regexp, $str, $match)) { + assertType('array{string, string}', $match); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12274.php b/tests/PHPStan/Analyser/nsrt/bug-12274.php new file mode 100644 index 0000000000..f0536a0c15 --- /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', $list); + } + assertType('array', $list); +} + +/** @param list $list */ +function testShouldLooseListbyAst2(array $list, int $i): void +{ + if (isset($list[$i])) { + assertType('list', $list); + $list[2+$i] = 21; + assertType('non-empty-array', $list); + } + assertType('array', $list); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12297.php b/tests/PHPStan/Analyser/nsrt/bug-12297.php new file mode 100644 index 0000000000..4a956e42a4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12297.php @@ -0,0 +1,19 @@ +', $value); return $value; } - assertType('mixed~array', $value); + assertType('mixed~array', $value); if (is_iterable($value)) { assertType('Traversable', $value); return iterator_to_array($value); } - assertType('mixed~array', $value); + assertType('mixed~array', $value); throw new \LogicException(); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12386.php b/tests/PHPStan/Analyser/nsrt/bug-12386.php new file mode 100644 index 0000000000..59d02b403f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12386.php @@ -0,0 +1,68 @@ +', $landMapper->fetchAllActivePrependDefault(12)); +} + +/** + * @template T of Clx_Model_Abstract + */ +abstract class Clx_Model_Mapper_Abstract +{ + public function __construct() + { + } +} + +/** + * @template T of Application_Model_Land + * + * @extends Clx_Model_Mapper_Abstract + */ +class ClxProductNet_Model_Mapper_Land extends Clx_Model_Mapper_Abstract +{ + /** + * @param int $defaultLandid + * + * @return Clx_Model_Iterator + */ + public function fetchAllActivePrependDefault($defaultLandid): Clx_Model_Iterator + {} +} + +/** + * @template T of Application_Model_Land + * + * @extends ClxProductNet_Model_Mapper_Land + */ +final class Application_Model_Mapper_Land extends ClxProductNet_Model_Mapper_Land +{ +} + +/** + * @template T of Clx_Model_Abstract + * + * @implements \Iterator + */ +abstract class Clx_Model_Iterator implements \Countable, \Iterator +{} + +abstract class Clx_Model_Abstract implements \Stringable +{} + +abstract class ClxProductNet_Model_Land extends Clx_Model_Abstract +{} + +final class Application_Model_Land extends ClxProductNet_Model_Land +{ + public function __toString() + { + return 'foo'; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393-php84.php b/tests/PHPStan/Analyser/nsrt/bug-12393-php84.php new file mode 100644 index 0000000000..b73906fdfd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12393-php84.php @@ -0,0 +1,23 @@ += 8.4 + +declare(strict_types = 1); + +namespace Bug12393Php84; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + + +class StringableFoo { + private string $foo; + + // https://3v4l.org/2SPPj#v8.4.6 + public function doFoo3(\BcMath\Number $foo): void { + $this->foo = $foo; + assertType('*NEVER*', $this->foo); + } + + public function __toString(): string { + return 'Foo'; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393.php b/tests/PHPStan/Analyser/nsrt/bug-12393.php new file mode 100644 index 0000000000..4edd2300c1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12393.php @@ -0,0 +1,184 @@ +name = $plugin["name"]; + assertType('string', $this->name); + } + + /** + * @param mixed[] $plugin + */ + public function doFoo(array $plugin){ + $this->untypedName = $plugin["name"]; + assertType('mixed', $this->untypedName); + } + + public function doBar(int $i){ + $this->float = $i; + assertType('float', $this->float); + } + + public function doBaz(int $i){ + $this->untypedFloat = $i; + assertType('int', $this->untypedFloat); + } + + public function doLorem(): void + { + $this->a = ['a' => 1]; + assertType('array{a: 1}', $this->a); + } + + public function doFloatTricky(){ + $this->float = 1; + assertType('1.0', $this->float); + } +} + +class HelloWorldStatic +{ + private static string $name; + + /** @var string */ + private static $untypedName; + + private static float $float; + + /** @var float */ + private static $untypedFloat; + + private static array $a; + + /** + * @param mixed[] $plugin + */ + public function __construct(array $plugin){ + self::$name = $plugin["name"]; + assertType('string', self::$name); + } + + /** + * @param mixed[] $plugin + */ + public function doFoo(array $plugin){ + self::$untypedName = $plugin["name"]; + assertType('mixed', self::$untypedName); + } + + public function doBar(int $i){ + self::$float = $i; + assertType('float', self::$float); + } + + public function doBaz(int $i){ + self::$untypedFloat = $i; + assertType('int', self::$untypedFloat); + } + + public function doLorem(): void + { + self::$a = ['a' => 1]; + assertType('array{a: 1}', self::$a); + } +} + +class EntryPointLookup +{ + + /** @var array|null */ + private ?array $entriesData = null; + + /** + * @return array + */ + public function doFoo(): void + { + if ($this->entriesData !== null) { + return; + } + + assertType('null', $this->entriesData); + assertNativeType('null', $this->entriesData); + + $data = $this->getMixed(); + if ($data !== null) { + $this->entriesData = $data; + assertType('array', $this->entriesData); + assertNativeType('array', $this->entriesData); + return; + } + + assertType('null', $this->entriesData); + assertNativeType('null', $this->entriesData); + } + + /** + * @return mixed + */ + public function getMixed() + { + + } + +} + +// https://3v4l.org/LK6Rh +class CallableString { + private string $foo; + + public function doFoo(callable $foo): void { + $this->foo = $foo; // PHPStorm wrongly reports an error on this line + assertType('callable-string|non-empty-string', $this->foo); + } +} + +// https://3v4l.org/WJ8NW +class CallableArray { + private array $foo; + + public function doFoo(callable $foo): void { + $this->foo = $foo; + assertType('array', $this->foo); // could be non-empty-array + } +} + +class StringableFoo { + private string $foo; + + // https://3v4l.org/DQSgA#v8.4.6 + public function doFoo(StringableFoo $foo): void { + $this->foo = $foo; + assertType('*NEVER*', $this->foo); + } + + public function doFoo2(NotStringable $foo): void { + $this->foo = $foo; + assertType('*NEVER*', $this->foo); + } + + public function __toString(): string { + return 'Foo'; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b-php84.php b/tests/PHPStan/Analyser/nsrt/bug-12393b-php84.php new file mode 100644 index 0000000000..ae1946cdb2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b-php84.php @@ -0,0 +1,22 @@ += 8.4 + +declare(strict_types = 0); + +namespace Bug12393bPhp84; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class StringableFoo { + private string $foo; + + // https://3v4l.org/nelJF#v8.4.6 + public function doFoo3(\BcMath\Number $foo): void { + $this->foo = $foo; + assertType('non-empty-string&numeric-string', $this->foo); + } + + public function __toString(): string { + return 'Foo'; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php new file mode 100644 index 0000000000..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-12473-types.php b/tests/PHPStan/Analyser/nsrt/bug-12473-types.php new file mode 100644 index 0000000000..d1f7a0a9cc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12473-types.php @@ -0,0 +1,48 @@ += 8.4 + +namespace Bug12473Types; + +use ReflectionClass; +use function PHPStan\Testing\assertType; + +class Picture +{ +} + +class PictureUser extends Picture +{ +} + +class PictureProduct extends Picture +{ +} + +/** + * @param class-string $a + */ +function doFoo(string $a): void +{ + $r = new ReflectionClass($a); + assertType('ReflectionClass', $r); + if ($r->isSubclassOf(Picture::class)) { + assertType('ReflectionClass', $r); + } else { + assertType('ReflectionClass', $r); + } + assertType('ReflectionClass|ReflectionClass', $r); +} + +/** + * @param class-string $a + */ +function doFoo2(string $a): void +{ + $r = new ReflectionClass($a); + assertType('ReflectionClass', $r); + if ($r->isSubclassOf(Picture::class)) { + assertType('ReflectionClass', $r); + } else { + assertType('*NEVER*', $r); + } + assertType('ReflectionClass', $r); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12575.php b/tests/PHPStan/Analyser/nsrt/bug-12575.php new file mode 100644 index 0000000000..f5199523d4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12575.php @@ -0,0 +1,85 @@ + $class + * @return $this + * @phpstan-self-out static + */ + public function add(string $class) + { + return $this; + } + +} + +/** + * @template T of object + * @extends Foo + */ +class Bar extends Foo +{ + + public function doFoo(): void + { + assertType('$this(Bug12575\Bar)&static(Bug12575\Bar)', $this->add(A::class)); + assertType('$this(Bug12575\Bar)&static(Bug12575\Bar)', $this); + assertType('T of object (class Bug12575\Bar, argument)', $this->getT()); + } + + public function doBar(): void + { + $this->add(B::class); + assertType('$this(Bug12575\Bar)&static(Bug12575\Bar)', $this); + assertType('T of object (class Bug12575\Bar, argument)', $this->getT()); + } + + /** + * @return T + */ + public function getT() + { + + } + +} + +interface A +{ + +} + +interface B +{ + +} + +/** + * @param Bar $bar + * @return void + */ +function doFoo(Bar $bar): void { + assertType('Bug12575\\Bar', $bar->add(B::class)); + assertType('Bug12575\\Bar', $bar); + assertType('Bug12575\A&Bug12575\B', $bar->getT()); +}; + +/** + * @param Bar $bar + * @return void + */ +function doBar(Bar $bar): void { + $bar->add(B::class); + assertType('Bug12575\\Bar', $bar); + assertType('Bug12575\A&Bug12575\B', $bar->getT()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-12660.php b/tests/PHPStan/Analyser/nsrt/bug-12660.php new file mode 100644 index 0000000000..44c79a0444 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12660.php @@ -0,0 +1,21 @@ + + */ + public function map($callable): self + { + return new self($callable($this->value)); + } + + /** + * @template S + * @param Closure(T): S $callable + * @return self + */ + public function mapClosure($callable): self + { + return new self($callable($this->value)); + } +} + +/** + * @param Option> $ints + */ +function doFoo(Option $ints): void { + assertType('Bug12691\\Option>', $ints->map(array_values(...))); + assertType('Bug12691\\Option>', $ints->map('array_values')); + assertType('Bug12691\\Option>', $ints->map(static fn ($value) => array_values($value))); +}; + +/** + * @param Option> $ints + */ +function doFooClosure(Option $ints): void { + assertType('Bug12691\\Option>', $ints->mapClosure(array_values(...))); + assertType('Bug12691\\Option>', $ints->mapClosure(static fn ($value) => array_values($value))); +}; + +/** + * @template T + * @param array $a + * @return ($a is non-empty-array ? non-empty-list : list) + */ +function myArrayValues(array $a): array { + +} + +/** + * @param Option> $ints + */ +function doBar(Option $ints): void { + assertType('Bug12691\\Option>', $ints->mapClosure(myArrayValues(...))); + assertType('Bug12691\\Option>', $ints->mapClosure(static fn ($value) => myArrayValues($value))); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-12731.php b/tests/PHPStan/Analyser/nsrt/bug-12731.php new file mode 100644 index 0000000000..79c8160461 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12731.php @@ -0,0 +1,24 @@ +', max(4, pure_int())); + +$_ = impure_int(); +assertType('int<4, max>', max(4, $_)); + +assertType('int<4, max>', max(4, impure_int())); +assertType('int<4, max>', max(impure_int(), 4)); +assertType('int', impure_int()); diff --git a/tests/PHPStan/Analyser/nsrt/bug-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/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-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-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-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..dc6d25a8b6 --- /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 @@ + 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-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-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-2112.php b/tests/PHPStan/Analyser/nsrt/bug-2112.php index a7d33d1538..65634c415b 100644 --- a/tests/PHPStan/Analyser/nsrt/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('hasOffsetValue(0, null)&non-empty-array', $foos); + assertType('non-empty-array&hasOffsetValue(0, null)', $foos); } /** @return self[] */ 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-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/nsrt/bug-2911.php b/tests/PHPStan/Analyser/nsrt/bug-2911.php index 1544279ea2..3cfbd308f1 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-2911.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2911.php @@ -59,7 +59,7 @@ private function getResultSettings(array $settings): array $settings['remove'] = strtolower($settings['remove']); - assertType("non-empty-array&hasOffsetValue('remove', string)", $settings); + assertType("non-empty-array&hasOffsetValue('remove', lowercase-string)", $settings); if (!in_array($settings['remove'], ['first', 'last', 'all'], true)) { throw $this->configException($settings, 'remove'); @@ -134,6 +134,6 @@ private function getResultSettings(array $settings): array function foo(array $array): void { $array['bar'] = 'string'; - assertType("hasOffsetValue('bar', 'string')&non-empty-array", $array); + assertType("non-empty-array&hasOffsetValue('bar', 'string')", $array); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-3133.php b/tests/PHPStan/Analyser/nsrt/bug-3133.php index 98eb5841f0..c6516dfe70 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3133.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3133.php @@ -52,7 +52,7 @@ public function doLorem( { $a = []; $a[$numericString] = 'foo'; - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-3276.php b/tests/PHPStan/Analyser/nsrt/bug-3276.php index ece368c9b1..53faad441b 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3276.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3276.php @@ -1,4 +1,4 @@ -= 7.4 +', 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()); + \PHPStan\Testing\assertType('array|false', mb_convert_encoding($arr)); + \PHPStan\Testing\assertType('string|false', mb_convert_encoding($str)); + \PHPStan\Testing\assertType('array|string|false', mb_convert_encoding($mixed)); + \PHPStan\Testing\assertType('array|string|false', mb_convert_encoding()); }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-3446.php b/tests/PHPStan/Analyser/nsrt/bug-3446.php index cc02033da3..c01b4bdbdc 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3446.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3446.php @@ -15,7 +15,7 @@ public function takesString(string $s) : void{} public function main2(string $input) : void{ if(is_array($var = json_decode($input))){ - assertType('array', $var); + assertType('array', $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/nsrt/bug-3789.php b/tests/PHPStan/Analyser/nsrt/bug-3789.php index 133ce49b50..95934512e3 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3789.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3789.php @@ -19,5 +19,5 @@ function doFoo(string $needle, array $haystack): void { assertType('0|1|2', array_search('foo', $haystack, true)); assertType('0|1|2|false', array_search($needle, $haystack)); - assertType('0|1|2|false', array_search('foo', $haystack)); + assertType('0|1|2', array_search('foo', $haystack)); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-3991.php b/tests/PHPStan/Analyser/nsrt/bug-3991.php index 3a01a2d27b..e1a79f6937 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3991.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3991.php @@ -26,7 +26,7 @@ public static function email($config = null) 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); + assertNativeType('mixed~(0|0.0|\'\'|\'0\'|array{}|stdClass|false|null)', $config); assertType('*NEVER*', $config); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-3993.php b/tests/PHPStan/Analyser/nsrt/bug-3993.php index e472a0d68c..a1a24e380d 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3993.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3993.php @@ -13,7 +13,7 @@ public function doFoo($arguments) return; } - assertType('mixed~null', $arguments); + assertType('mixed~(array{}|null)', $arguments); array_shift($arguments); diff --git a/tests/PHPStan/Analyser/nsrt/bug-4099.php b/tests/PHPStan/Analyser/nsrt/bug-4099.php index 0a8d1b4b48..5e5eb30ca2 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4099.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4099.php @@ -22,20 +22,20 @@ function arrayHint(array $arr): void throw new \Exception('no key "key" found.'); } assertType('array{key: array{inner: mixed}}', $arr); - assertNativeType('array&hasOffset(\'key\')', $arr); + assertNativeType('non-empty-array&hasOffset(\'key\')', $arr); assertType('array{inner: mixed}', $arr['key']); assertNativeType('mixed', $arr['key']); if (!array_key_exists('inner', $arr['key'])) { assertType('*NEVER*', $arr); - assertNativeType('array&hasOffset(\'key\')', $arr); + assertNativeType('non-empty-array&hasOffset(\'key\')', $arr); assertType('*NEVER*', $arr['key']); assertNativeType("mixed~hasOffset('inner')", $arr['key']); throw new \Exception('need key.inner'); } assertType('array{key: array{inner: mixed}}', $arr); - assertNativeType('array&hasOffset(\'key\')', $arr); + assertNativeType('non-empty-array&hasOffset(\'key\')', $arr); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4117.php b/tests/PHPStan/Analyser/nsrt/bug-4117.php index d1bca857c3..14732b77b6 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4117.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4117.php @@ -32,9 +32,9 @@ 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); + assertType("T of mixed~(0|0.0|''|'0'|array{}|false|null) (class Bug4117Types\GenericList, argument)", $item); } else { - assertType("(array{}&T of mixed~null (class Bug4117Types\GenericList, argument))|(0.0&T of mixed~null (class Bug4117Types\GenericList, argument))|(0&T of mixed~null (class Bug4117Types\GenericList, argument))|(''&T of mixed~null (class Bug4117Types\GenericList, argument))|('0'&T of mixed~null (class Bug4117Types\GenericList, argument))|(T of mixed~null (class Bug4117Types\GenericList, argument)&false)|null", $item); + assertType("(T 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); diff --git a/tests/PHPStan/Analyser/nsrt/bug-4188.php b/tests/PHPStan/Analyser/nsrt/bug-4188.php index 3a7c514215..a34cca26a4 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4188.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4188.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 +', array_keys($meters)); - assertType('non-empty-list', array_values($meters)); + assertType('non-empty-list', array_values($meters)); }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4434.php b/tests/PHPStan/Analyser/nsrt/bug-4434.php index a1f3bea048..7b48df20fd 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4434.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4434.php @@ -10,14 +10,14 @@ class HelloWorld public function testSendEmailToLog(): void { foreach ([1] as $emailFile) { - assertType('int<5, max>', PHP_MAJOR_VERSION); - assertType('int<5, max>', \PHP_MAJOR_VERSION); + assertType('int<5, 8>', 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('int<5, 6>|int<8, max>', PHP_MAJOR_VERSION); - assertType('int<5, 6>|int<8, max>', \PHP_MAJOR_VERSION); + assertType('8|int<5, 6>', PHP_MAJOR_VERSION); + assertType('8|int<5, 6>', \PHP_MAJOR_VERSION); } } } @@ -28,14 +28,14 @@ class HelloWorld2 public function testSendEmailToLog(): void { foreach ([1] as $emailFile) { - assertType('int<5, max>', PHP_MAJOR_VERSION); - assertType('int<5, max>', \PHP_MAJOR_VERSION); + assertType('int<5, 8>', PHP_MAJOR_VERSION); + assertType('int<5, 8>', \PHP_MAJOR_VERSION); if (PHP_MAJOR_VERSION === 100) { - assertType('100', PHP_MAJOR_VERSION); - assertType('100', \PHP_MAJOR_VERSION); + assertType('*NEVER*', PHP_MAJOR_VERSION); + assertType('*NEVER*', \PHP_MAJOR_VERSION); } else { - assertType('int<5, 99>|int<101, max>', PHP_MAJOR_VERSION); - assertType('int<5, 99>|int<101, max>', \PHP_MAJOR_VERSION); + assertType('int<5, 8>', PHP_MAJOR_VERSION); + assertType('int<5, 8>', \PHP_MAJOR_VERSION); } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4565.php b/tests/PHPStan/Analyser/nsrt/bug-4565.php index af941f8098..48ab02dd92 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4565.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4565.php @@ -10,10 +10,10 @@ function test(array $variables) { if (!empty($variables['button'])) { assertType('non-empty-array', $attributes); $attributes['type'] = 'button'; - assertType("hasOffsetValue('type', 'button')&non-empty-array", $attributes); + assertType("non-empty-array&hasOffsetValue('type', 'button')", $attributes); unset($attributes['href']); - assertType("array&hasOffsetValue('type', 'button')", $attributes); + assertType("non-empty-array&hasOffsetValue('type', 'button')", $attributes); } - assertType('array', $attributes); + assertType('non-empty-array', $attributes); return $attributes; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4587.php b/tests/PHPStan/Analyser/nsrt/bug-4587.php index 644b9d7b79..061c953a28 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4587.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4587.php @@ -27,11 +27,11 @@ public function b(): void $type = array_map(static function (array $result): array { assertType('array{a: int}', $result); $result['a'] = (string) $result['a']; - assertType('array{a: numeric-string}', $result); + assertType('array{a: lowercase-string&numeric-string&uppercase-string}', $result); return $result; }, $results); - assertType('list', $type); + assertType('list', $type); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4700.php b/tests/PHPStan/Analyser/nsrt/bug-4700.php index 078ea41b12..9d386b0c50 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4700.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4700.php @@ -40,10 +40,10 @@ function(array $array, int $count): void { 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); + 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('0', count($a)); - assertType('array{}', $a); + assertType('int<0, 5>', count($a)); // Could be int<0, 1> + assertType('array{}|array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); // Could be array{}|array{0: mixed~null} } }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4708.php b/tests/PHPStan/Analyser/nsrt/bug-4708.php index d6bae28afc..b6f2302722 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4708.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4708.php @@ -57,7 +57,7 @@ function GetASCConfig() } else { - assertType("array&hasOffsetValue('bsw', string)", $result); + assertType("non-empty-array&hasOffsetValue('bsw', string)", $result); $result['bsw'] = (int) $result['bsw']; assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4711.php b/tests/PHPStan/Analyser/nsrt/bug-4711.php index 46bc69565f..9050c74c15 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4711.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4711.php @@ -12,8 +12,8 @@ function x(string $string): void { return; } - assertType('non-empty-list', explode($string, '')); - assertType('non-empty-list', explode($string[0], '')); + assertType('non-empty-list', explode($string, '')); + assertType('non-empty-list', explode($string[0], '')); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4879.php b/tests/PHPStan/Analyser/nsrt/bug-4879.php index 1c6c9536c4..26d74b1c73 100644 --- a/tests/PHPStan/Analyser/nsrt/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-5017.php b/tests/PHPStan/Analyser/nsrt/bug-5017.php index fa1abc5b46..c4e7cfebaa 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-5017.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5017.php @@ -12,10 +12,10 @@ public function doFoo() $items = [0, 1, 2, 3, 4]; while ($items) { - assertType('non-empty-array<0|1|2|3|4, 0|1|2|3|4>', $items); + assertType('non-empty-list>', $items); $batch = array_splice($items, 0, 2); - assertType('array<0|1|2|3|4, 0|1|2|3|4>', $items); - assertType('array<0|1|2|3|4, 0|1|2|3|4>', $batch); + assertType('list>', $items); + assertType('non-empty-list>', $batch); } } @@ -37,8 +37,8 @@ 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); + assertType('array{2, 3, 4}', $items); + assertType('array{0, 1}', $batch); } /** 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-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-5219.php b/tests/PHPStan/Analyser/nsrt/bug-5219.php index 39d1540111..f7431de04c 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-5219.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5219.php @@ -20,6 +20,6 @@ protected function bar(string $message): void $header = sprintf('%s-%s', '', ''); assertType('\'-\'', $header); - assertType('array{-: string}', [$header => $message]); + assertType('array{\'-\': string}', [$header => $message]); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-5287-php81.php b/tests/PHPStan/Analyser/nsrt/bug-5287-php81.php index 7d57aea2c0..d1cb994c92 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-5287-php81.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5287-php81.php @@ -48,7 +48,7 @@ function foo4(array $arr): void function foo5(array $arr): void { $arrSpread = [...$arr]; - assertType('non-empty-array', $arrSpread); + assertType('non-empty-array', $arrSpread); } /** 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-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-6293.php b/tests/PHPStan/Analyser/nsrt/bug-6293.php index 0a5c8548be..993f7b470e 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6293.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6293.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug6239; diff --git a/tests/PHPStan/Analyser/nsrt/bug-6329.php b/tests/PHPStan/Analyser/nsrt/bug-6329.php index c6b49e87e4..b31842e05f 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6329.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6329.php @@ -106,19 +106,19 @@ function true($a): void function nonEmptyArray1($a): void { if (is_array($a) && [] !== $a || null === $a) { - assertType('non-empty-array|null', $a); + assertType('non-empty-array|null', $a); } if ([] !== $a && is_array($a) || null === $a) { - assertType('non-empty-array|null', $a); + assertType('non-empty-array|null', $a); } if (null === $a || is_array($a) && [] !== $a) { - assertType('non-empty-array|null', $a); + assertType('non-empty-array|null', $a); } if (null === $a || [] !== $a && is_array($a)) { - assertType('non-empty-array|null', $a); + assertType('non-empty-array|null', $a); } } @@ -128,11 +128,11 @@ function nonEmptyArray1($a): void function nonEmptyArray2($a): void { if (is_array($a) && count($a) > 0 || null === $a) { - assertType('non-empty-array|null', $a); + assertType('non-empty-array|null', $a); } if (null === $a || is_array($a) && count($a) > 0) { - assertType('non-empty-array|null', $a); + assertType('non-empty-array|null', $a); } } @@ -155,7 +155,7 @@ function inverse($a, $b, $c): void if (null !== $c && (!is_array($c) || count($c) <= 0)) { } else { - assertType('non-empty-array|null', $c); + assertType('non-empty-array|null', $c); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-6462.php b/tests/PHPStan/Analyser/nsrt/bug-6462.php index 84c475d48a..51854608d7 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6462.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6462.php @@ -37,21 +37,19 @@ public function getThis(): self } } -$base = new Base(); -$child = new Child(); -$fixedChild = new FixedChild(); +function (Base $base, Child $child, FixedChild $fixedChild): void { + assertType('Bug6462\Base', $base->getThis()); + assertType('Bug6462\Child', $child->getThis()); -assertType('Bug6462\Base', $base->getThis()); -assertType('Bug6462\Child', $child->getThis()); - -if ($base instanceof \Traversable) { - assertType('Bug6462\Base&Traversable', $base->getThis()); -} + if ($base instanceof \Traversable) { + assertType('Bug6462\Base&Traversable', $base->getThis()); + } -if ($child instanceof \Traversable) { - assertType('Bug6462\Child&Traversable', $child->getThis()); -} + if ($child instanceof \Traversable) { + assertType('Bug6462\Child&Traversable', $child->getThis()); + } -if ($fixedChild instanceof \Traversable) { - assertType('Bug6462\FixedChild&Traversable', $fixedChild->getThis()); -} + if ($fixedChild instanceof \Traversable) { + assertType('Bug6462\FixedChild&Traversable', $fixedChild->getThis()); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-6609-83.php b/tests/PHPStan/Analyser/nsrt/bug-6609-83.php index 65d7f22f1d..4a5f5bb781 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6609-83.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6609-83.php @@ -50,7 +50,7 @@ function modify2(\DateTimeInterface $date) { */ function modify3(\DateTimeInterface $date, string $s) { $date = $date->modify($s); - assertType('DateTime|DateTimeImmutable', $date); + 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 index 046f9c8403..571f97d988 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6609.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6609.php @@ -50,7 +50,7 @@ function modify2(\DateTimeInterface $date) { */ function modify3(\DateTimeInterface $date, string $s) { $date = $date->modify($s); - assertType('(DateTime|DateTimeImmutable|false)', $date); + 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 index 29898f7532..20abbe4b24 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6613.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6613.php @@ -6,6 +6,10 @@ function (\DateTime $dt) { assertType("'000000'", date('u')); - assertType('non-falsy-string', date_format($dt, 'u')); - assertType('non-falsy-string', $dt->format('u')); + assertType('non-falsy-string&numeric-string', date_format($dt, 'u')); + assertType('non-falsy-string&numeric-string', $dt->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-6859.php b/tests/PHPStan/Analyser/nsrt/bug-6859.php index f12e61342f..56cd257c5e 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6859.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6859.php @@ -1,4 +1,4 @@ -= 7.4 +', array_values($body)); + assertType('non-empty-list', array_values($body)); } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-7141.php b/tests/PHPStan/Analyser/nsrt/bug-7141.php index 277b00d9e6..2cf34a5733 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7141.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7141.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types = 1); namespace Bug7141; diff --git a/tests/PHPStan/Analyser/nsrt/bug-7144-composer-integration.php b/tests/PHPStan/Analyser/nsrt/bug-7144-composer-integration.php index 057067411c..c60d776d38 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7144-composer-integration.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7144-composer-integration.php @@ -32,14 +32,14 @@ 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); + \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); + \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); + \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); + \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-7176.php b/tests/PHPStan/Analyser/nsrt/bug-7176.php index 6be67ffaba..29a5f83184 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7176.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7176.php @@ -25,7 +25,7 @@ function test(Suit $x): string { assertType('Bug7176Types\Suit::Spades', $x); return 'DOES NOT WORK'; } - assertType('Bug7176Types\Suit~Bug7176Types\Suit::Clubs|Bug7176Types\Suit::Spades', $x); + assertType('Bug7176Types\Suit~(Bug7176Types\Suit::Clubs|Bug7176Types\Suit::Spades)', $x); return match ($x) { Suit::Hearts => 'a', diff --git a/tests/PHPStan/Analyser/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 index a227b7cf3c..45b3efe97b 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7341.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7341.php @@ -1,4 +1,4 @@ - $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)); @@ -19,28 +22,31 @@ public function inputTypes(int $i, float $f, string $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('numeric-string', sprintf('%14s', $i)); + assertType('lowercase-string&numeric-string&uppercase-string', sprintf('%14s', $i)); - assertType('numeric-string', sprintf('%d', $i)); + assertType('lowercase-string&numeric-string', sprintf('%d', $i)); - assertType('numeric-string', sprintf('%14b', $i)); - assertType('non-falsy-string', sprintf('%14c', $i)); // binary string - assertType('numeric-string', sprintf('%14d', $i)); - assertType('numeric-string', sprintf('%14e', $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('numeric-string', sprintf('%14f', $i)); + assertType('lowercase-string&numeric-string', sprintf('%14f', $i)); assertType('numeric-string', sprintf('%14F', $i)); - assertType('numeric-string', sprintf('%14g', $i)); + assertType('lowercase-string&numeric-string', sprintf('%14g', $i)); assertType('numeric-string', sprintf('%14G', $i)); - assertType('numeric-string', sprintf('%14h', $i)); + assertType('lowercase-string&numeric-string', sprintf('%14h', $i)); assertType('numeric-string', sprintf('%14H', $i)); - assertType('numeric-string', sprintf('%14o', $i)); - assertType('numeric-string', sprintf('%14u', $i)); - assertType('numeric-string', sprintf('%14x', $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)); } @@ -53,11 +59,27 @@ public function specifiers(int $i) { */ public function positionalArgs($mixed, int $i, float $f, string $s, int $posInt, int $negInt, int $nonZeroIntRange, int $intRange) { // https://3v4l.org/vVL0c - assertType('numeric-string', sprintf('%2$14s', $mixed, $i)); - assertType('non-falsy-string&numeric-string', sprintf('%2$14s', $mixed, $posInt)); - assertType('non-falsy-string&numeric-string', sprintf('%2$14s', $mixed, $negInt)); - assertType('numeric-string', sprintf('%2$14s', $mixed, $intRange)); - assertType('non-falsy-string&numeric-string', sprintf('%2$14s', $mixed, $nonZeroIntRange)); + 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)); @@ -80,16 +102,16 @@ public function invalidPositionalArgFormat($mixed, string $s) { public function escapedPercent(int $i) { // https://3v4l.org/2m50L - assertType('non-falsy-string', sprintf("%%d", $i)); + assertType('lowercase-string&non-falsy-string', sprintf("%%d", $i)); } public function vsprintf(array $array) { - assertType('numeric-string', vsprintf("%4d", explode('-', '1988-8-1'))); + assertType('lowercase-string&numeric-string', vsprintf("%4d", explode('-', '1988-8-1'))); assertType('numeric-string', vsprintf("%4d", $array)); - assertType('numeric-string', vsprintf("%4d", ['123'])); - assertType('string', vsprintf("%s", ['123'])); // could be '123' + assertType('lowercase-string&numeric-string', vsprintf("%4d", ['123'])); + assertType('\'123\'', vsprintf("%s", ['123'])); // too many arguments.. php silently allows it - assertType('numeric-string', vsprintf("%4d", ['123', '456'])); + assertType('lowercase-string&numeric-string', vsprintf("%4d", ['123', '456'])); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-7580-php82.php b/tests/PHPStan/Analyser/nsrt/bug-7580-php82.php new file mode 100644 index 0000000000..40031601b4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7580-php82.php @@ -0,0 +1,20 @@ += 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 index 26fdbc18cb..1bfc0b7544 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7580.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7580.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types = 1); namespace Bug7607; @@ -28,7 +30,7 @@ public function blank($value) return false; } - if ($value instanceof Countable) { + if ($value instanceof \Countable) { return count($value) === 0; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-7621-1.php b/tests/PHPStan/Analyser/nsrt/bug-7621-1.php index e40ff83e62..26fab127a6 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7621-1.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7621-1.php @@ -134,7 +134,7 @@ public function unpackShortcut(string $shortcut, array $supportedGroups): array !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{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)); diff --git a/tests/PHPStan/Analyser/nsrt/bug-7685.php b/tests/PHPStan/Analyser/nsrt/bug-7685.php new file mode 100644 index 0000000000..e5674250b4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7685.php @@ -0,0 +1,17 @@ += 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-7788.php b/tests/PHPStan/Analyser/nsrt/bug-7788.php index 944d10e7e4..fa5c6a73af 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7788.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7788.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug7788; diff --git a/tests/PHPStan/Analyser/nsrt/bug-7805.php b/tests/PHPStan/Analyser/nsrt/bug-7805.php index cf96fe46d5..ec9464ebd3 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7805.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7805.php @@ -14,17 +14,17 @@ function foo(array $params) assertNativeType('array', $params); if (array_key_exists('help', $params)) { assertType('array{help: null}', $params); - assertNativeType("array&hasOffset('help')", $params); + assertNativeType("non-empty-array&hasOffset('help')", $params); unset($params['help']); assertType('array{}', $params); assertNativeType("array", $params); $params = $params === [] ? ['list'] : $params; assertType("array{'list'}", $params); - assertNativeType("non-empty-array", $params); + assertNativeType("array{'list'}", $params); array_unshift($params, 'help'); assertType("array{'help', 'list'}", $params); - assertNativeType("non-empty-array", $params); + assertNativeType("array{'help', 'list'}", $params); } assertType("array{}|array{'help', 'list'}", $params); assertNativeType('array', $params); diff --git a/tests/PHPStan/Analyser/nsrt/bug-7809.php b/tests/PHPStan/Analyser/nsrt/bug-7809.php index 2769152110..a6c0c139cb 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7809.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7809.php @@ -6,7 +6,7 @@ use function PHPStan\Testing\assertType; function foo(bool $strict = false): void { - assertType('0|1|2|false', array_search('c', ['a', 'b', 'c'], $strict)); + assertType('2', array_search('c', ['a', 'b', 'c'], $strict)); } function bar(): void{ @@ -14,5 +14,5 @@ function bar(): void{ } function baz(): void{ - assertType('0|1|2|false', array_search('c', ['a', 'b', 'c'], false)); + assertType('2', array_search('c', ['a', 'b', 'c'], false)); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-7915.php b/tests/PHPStan/Analyser/nsrt/bug-7915.php index 1dee1a63d5..a2e63375a7 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7915.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7915.php @@ -63,7 +63,7 @@ public function setConfigMimicConditionalParamType($name, $value): void function to_utf8($data, bool $isArray = false) { if ($isArray) { - assertType('array', $data); + assertType('array', $data); if (is_array($data)) { // always true foreach ($data as $k => $value) { $data[$k] = to_utf8($value, is_array($value)); diff --git a/tests/PHPStan/Analyser/nsrt/bug-7944.php b/tests/PHPStan/Analyser/nsrt/bug-7944.php index 737ab3dcb8..0d219b40b3 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7944.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7944.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug7944; diff --git a/tests/PHPStan/Analyser/nsrt/bug-8249.php b/tests/PHPStan/Analyser/nsrt/bug-8249.php index 960126723d..46964dc0ad 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-8249.php +++ b/tests/PHPStan/Analyser/nsrt/bug-8249.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug8249; 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 index 71db7a6c98..9236447acf 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-8568.php +++ b/tests/PHPStan/Analyser/nsrt/bug-8568.php @@ -8,7 +8,7 @@ class HelloWorld { public function sayHello(): void { - assertType('non-falsy-string', 'a' . $this->get()); + assertType('lowercase-string&non-falsy-string', 'a' . $this->get()); } public function get(): ?int 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-8635.php b/tests/PHPStan/Analyser/nsrt/bug-8635.php index 1895254579..46d5b728b8 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-8635.php +++ b/tests/PHPStan/Analyser/nsrt/bug-8635.php @@ -8,6 +8,6 @@ class HelloWorld { public function EchoInt(int $value): void { - assertType('numeric-string', "$value"); + assertType('lowercase-string&numeric-string&uppercase-string', "$value"); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-8803.php b/tests/PHPStan/Analyser/nsrt/bug-8803.php index a1d9ad568b..88af4df14d 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-8803.php +++ b/tests/PHPStan/Analyser/nsrt/bug-8803.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug8803; 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-9062.php b/tests/PHPStan/Analyser/nsrt/bug-9062.php index a4c8cc6251..7280c8634c 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9062.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9062.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug9062; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9086.php b/tests/PHPStan/Analyser/nsrt/bug-9086.php index db0110f2f4..e099f4eec1 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9086.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9086.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug9086; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9105.php b/tests/PHPStan/Analyser/nsrt/bug-9105.php index 956d53f055..296baba23d 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9105.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9105.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug9105; 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-9341.php b/tests/PHPStan/Analyser/nsrt/bug-9341.php index 2c1a90f5bd..3265c4a7b0 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9341.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9341.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types = 1); namespace Bug9341; 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 index 923c0534e6..e81f67b7ea 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9472.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9472.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug9472; 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.php b/tests/PHPStan/Analyser/nsrt/bug-9662.php index 4da94355c7..d88555a863 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9662.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9662.php @@ -11,74 +11,74 @@ */ function doFoo(string $s, $a, $strings, $mixed) { if (in_array('foo', $a, true)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array('foo', $a, false)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array('foo', $a)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array('0', $a)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array('1', $a)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array(true, $a)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array(false, $a)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array($s, $a, true)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array($s, $a, false)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array($s, $a)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array($mixed, $strings, true)) { assertType('non-empty-array', $strings); diff --git a/tests/PHPStan/Analyser/nsrt/bug-9714.php b/tests/PHPStan/Analyser/nsrt/bug-9714.php index 3dbb7d1b87..5629966029 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9714.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9714.php @@ -10,7 +10,6 @@ public function exampleFunction(): void { $xml = new \SimpleXmlElement(''); $elements = $xml->xpath('//data'); - assertType('array', $elements); + assertType('array|null', $elements); } } - diff --git a/tests/PHPStan/Analyser/nsrt/bug-9734.php b/tests/PHPStan/Analyser/nsrt/bug-9734.php index ce75fa7197..1628ad6859 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9734.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9734.php @@ -15,9 +15,9 @@ class Foo public function doFoo(array $a): void { if (array_is_list($a)) { - assertType('list', $a); + assertType('list', $a); } else { - assertType('array', $a); // could be non-empty-array + assertType('array', $a); // could be non-empty-array } } @@ -38,9 +38,9 @@ public function doFoo2(): void public function doFoo3(array $a): void { if (array_is_list($a)) { - assertType('non-empty-list', $a); + assertType('non-empty-list', $a); } else { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-9764.php b/tests/PHPStan/Analyser/nsrt/bug-9764.php index 15807d0b1e..f24b810fe8 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9764.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9764.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug9764; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9867.php b/tests/PHPStan/Analyser/nsrt/bug-9867.php index 7c677aa8d6..6ab9515b87 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9867.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9867.php @@ -1,4 +1,4 @@ -= 8.0 declare(strict_types=1); diff --git a/tests/PHPStan/Analyser/nsrt/bug-9985.php b/tests/PHPStan/Analyser/nsrt/bug-9985.php index edbfebffc5..09a7ad92ea 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9985.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9985.php @@ -20,6 +20,6 @@ function (): void { assertType('array{}|array{a?: true, b: true}|array{a?: true, c?: true}', $warnings); if (!empty($warnings)) { - assertType('array{a?: true, b: true}|(array{a?: true, c?: true}&non-empty-array)', $warnings); + assertType('array{a?: true, b: true}|non-empty-array{a?: true, c?: true}', $warnings); } }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-pr-339.php b/tests/PHPStan/Analyser/nsrt/bug-pr-339.php index 768efbc147..4d841ad783 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-pr-339.php +++ b/tests/PHPStan/Analyser/nsrt/bug-pr-339.php @@ -17,14 +17,14 @@ assertType('mixed', $a); assertType('mixed', $c); if ($a) { - assertType("mixed~0|0.0|''|'0'|array{}|false|null", $a); + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $a); assertType('mixed', $c); assertVariableCertainty(TrinaryLogic::createYes(), $a); } if ($c) { assertType('mixed', $a); - assertType("mixed~0|0.0|''|'0'|array{}|false|null", $c); + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $c); assertVariableCertainty(TrinaryLogic::createYes(), $c); } } else { diff --git a/tests/PHPStan/Analyser/nsrt/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 index 12020de0b9..709f298635 100644 --- a/tests/PHPStan/Analyser/nsrt/bug11384.php +++ b/tests/PHPStan/Analyser/nsrt/bug11384.php @@ -14,7 +14,7 @@ class HelloWorld public function sayHello(string $s): void { if (preg_match('{(' . Bar::VAL . ')}', $s, $m)) { - assertType("array{string, '3'}", $m); + assertType("array{non-empty-string, '3'}", $m); } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug11480.php b/tests/PHPStan/Analyser/nsrt/bug11480.php index f4d3898790..17077d7bfc 100644 --- a/tests/PHPStan/Analyser/nsrt/bug11480.php +++ b/tests/PHPStan/Analyser/nsrt/bug11480.php @@ -84,7 +84,7 @@ public function intUnionCount(): void if (count($x) >= $count) { assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); } else { - assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + assertType("array{}", $x); } assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); } 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/callable-in-union.php b/tests/PHPStan/Analyser/nsrt/callable-in-union.php index 24db62b428..724ce9cafa 100644 --- a/tests/PHPStan/Analyser/nsrt/callable-in-union.php +++ b/tests/PHPStan/Analyser/nsrt/callable-in-union.php @@ -1,4 +1,4 @@ -= 7.4 +|numeric-string|true', $mixed); } else { - assertType('mixed~int<0, max>|numeric-string|true', $mixed); + assertType('mixed~(int<0, max>|numeric-string|true)', $mixed); } assertType('mixed', $mixed); diff --git a/tests/PHPStan/Analyser/nsrt/cast-to-numeric-string.php b/tests/PHPStan/Analyser/nsrt/cast-to-numeric-string.php index c7d6fcb84c..dfe47aa39b 100644 --- a/tests/PHPStan/Analyser/nsrt/cast-to-numeric-string.php +++ b/tests/PHPStan/Analyser/nsrt/cast-to-numeric-string.php @@ -13,13 +13,13 @@ * @param 1 $constantInt */ function foo(int $a, float $b, $numeric, $numeric2, $number, $positive, $negative, $constantInt): void { - assertType('numeric-string', (string)$a); - assertType('numeric-string', (string)$b); + assertType('lowercase-string&numeric-string&uppercase-string', (string)$a); + assertType('numeric-string&uppercase-string', (string)$b); assertType('numeric-string', (string)$numeric); assertType('numeric-string', (string)$numeric2); - assertType('numeric-string', (string)$number); - assertType('non-falsy-string&numeric-string', (string)$positive); - assertType('non-falsy-string&numeric-string', (string)$negative); + assertType('numeric-string&uppercase-string', (string)$number); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', (string)$positive); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', (string)$negative); assertType("'1'", (string)$constantInt); } @@ -32,32 +32,32 @@ function foo(int $a, float $b, $numeric, $numeric2, $number, $positive, $negativ * @param 1 $constantInt */ function concatEmptyString(int $a, float $b, $numeric, $numeric2, $number, $positive, $negative, $constantInt): void { - assertType('numeric-string', '' . $a); - assertType('numeric-string', '' . $b); + assertType('lowercase-string&numeric-string&uppercase-string', '' . $a); + assertType('numeric-string&uppercase-string', '' . $b); assertType('numeric-string', '' . $numeric); assertType('numeric-string', '' . $numeric2); - assertType('numeric-string', '' . $number); - assertType('non-falsy-string&numeric-string', '' . $positive); - assertType('non-falsy-string&numeric-string', '' . $negative); + assertType('numeric-string&uppercase-string', '' . $number); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', '' . $positive); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', '' . $negative); assertType("'1'", '' . $constantInt); - assertType('numeric-string', $a . ''); - assertType('numeric-string', $b . ''); + assertType('lowercase-string&numeric-string&uppercase-string', $a . ''); + assertType('numeric-string&uppercase-string', $b . ''); assertType('numeric-string', $numeric . ''); assertType('numeric-string', $numeric2 . ''); - assertType('numeric-string', $number . ''); - assertType('non-falsy-string&numeric-string', $positive . ''); - assertType('non-falsy-string&numeric-string', $negative . ''); + assertType('numeric-string&uppercase-string', $number . ''); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $positive . ''); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $negative . ''); assertType("'1'", $constantInt . ''); } function concatAssignEmptyString(int $i, float $f) { $i .= ''; - assertType('numeric-string', $i); + assertType('lowercase-string&numeric-string&uppercase-string', $i); $s = ''; $s .= $f; - assertType('numeric-string', $s); + assertType('numeric-string&uppercase-string', $s); } /** @@ -66,13 +66,13 @@ function concatAssignEmptyString(int $i, float $f) { */ function integerRangeToString($positive, $negative) { - assertType('numeric-string', (string) $positive); - assertType('numeric-string', (string) $negative); + assertType('lowercase-string&numeric-string&uppercase-string', (string) $positive); + assertType('lowercase-string&numeric-string&uppercase-string', (string) $negative); if ($positive !== 0) { - assertType('non-falsy-string&numeric-string', (string) $positive); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', (string) $positive); } if ($negative !== 0) { - assertType('non-falsy-string&numeric-string', (string) $negative); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', (string) $negative); } } diff --git a/tests/PHPStan/Analyser/nsrt/class-constant-types.php b/tests/PHPStan/Analyser/nsrt/class-constant-types.php index 9d60af25c5..8f19f7659e 100644 --- a/tests/PHPStan/Analyser/nsrt/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 index acd6a616ae..316c8e8ed4 100644 --- a/tests/PHPStan/Analyser/nsrt/class-implements.php +++ b/tests/PHPStan/Analyser/nsrt/class-implements.php @@ -1,4 +1,4 @@ -= 8.0 namespace ClassImplements; 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/nsrt/closure-argument-type.php b/tests/PHPStan/Analyser/nsrt/closure-argument-type.php index 6fd537211d..b24570b298 100644 --- a/tests/PHPStan/Analyser/nsrt/closure-argument-type.php +++ b/tests/PHPStan/Analyser/nsrt/closure-argument-type.php @@ -35,6 +35,10 @@ public function doFoo(int $integer, array $array, ?string $nullableString) assertType('array{a: int}', $context2); assertType('mixed', $context3); })($integer, $array); + + ($callback = function($context) { + assertType('int', $context); + })($integer); } } diff --git a/tests/PHPStan/Analyser/nsrt/composer-array-bug.php b/tests/PHPStan/Analyser/nsrt/composer-array-bug.php index 42733805c3..0873226897 100644 --- a/tests/PHPStan/Analyser/nsrt/composer-array-bug.php +++ b/tests/PHPStan/Analyser/nsrt/composer-array-bug.php @@ -16,7 +16,10 @@ class Foo public function doFoo(): void { if (!empty($this->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']); @@ -44,17 +47,17 @@ public function doFoo(): void } } - assertType("array&hasOffsetValue('authors', mixed)", $this->config); + 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("array&hasOffsetValue('authors', mixed~0|0.0|''|'0'|array{}|false|null)", $this->config); + assertType("non-empty-array&hasOffsetValue('authors', mixed~(0|0.0|''|'0'|array{}|false|null))", $this->config); } - assertType('array', $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 index 31914d185e..0cec47c79a 100644 --- a/tests/PHPStan/Analyser/nsrt/composer-non-empty-array-after-unset.php +++ b/tests/PHPStan/Analyser/nsrt/composer-non-empty-array-after-unset.php @@ -13,7 +13,7 @@ class Foo public function doFoo() { if (!empty($this->config['authors'])) { - assertType("mixed~0|0.0|''|'0'|array{}|false|null", $this->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)) { diff --git a/tests/PHPStan/Analyser/nsrt/conditional-types-inference.php b/tests/PHPStan/Analyser/nsrt/conditional-types-inference.php index 55335c6e2e..596e97634a 100644 --- a/tests/PHPStan/Analyser/nsrt/conditional-types-inference.php +++ b/tests/PHPStan/Analyser/nsrt/conditional-types-inference.php @@ -1,4 +1,4 @@ -= 8.0 namespace ConditionalTypesInference; @@ -89,5 +89,5 @@ function invariant(bool $condition, string $message): void function (mixed $value) { invariant(is_array($value), 'must be array'); - assertType('array', $value); + assertType('array', $value); }; diff --git a/tests/PHPStan/Analyser/nsrt/conditional-vars.php b/tests/PHPStan/Analyser/nsrt/conditional-vars.php index 6d86c88014..568c6a8b7f 100644 --- a/tests/PHPStan/Analyser/nsrt/conditional-vars.php +++ b/tests/PHPStan/Analyser/nsrt/conditional-vars.php @@ -10,27 +10,29 @@ class HelloWorld public function conditionalVarInTernary(array $innerHits): void { if (array_key_exists('nearest_premise', $innerHits) || array_key_exists('matching_premises', $innerHits)) { - assertType('array', $innerHits); + assertType('non-empty-array', $innerHits); $x = array_key_exists('nearest_premise', $innerHits) - ? assertType("array&hasOffset('nearest_premise')", $innerHits) - : assertType('array', $innerHits); + ? assertType("non-empty-array&hasOffset('nearest_premise')", $innerHits) + : assertType('non-empty-array', $innerHits); - assertType('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('array', $innerHits); + assertType('non-empty-array', $innerHits); if (array_key_exists('nearest_premise', $innerHits)) { - assertType("array&hasOffset('nearest_premise')", $innerHits); + assertType("non-empty-array&hasOffset('nearest_premise')", $innerHits); } else { - assertType('array', $innerHits); + assertType('non-empty-array', $innerHits); } - assertType('array', $innerHits); + assertType('non-empty-array', $innerHits); } + assertType('array', $innerHits); } } diff --git a/tests/PHPStan/Analyser/nsrt/constant-array-optional-set.php b/tests/PHPStan/Analyser/nsrt/constant-array-optional-set.php index 6faaa574c1..fe3512a45b 100644 --- a/tests/PHPStan/Analyser/nsrt/constant-array-optional-set.php +++ b/tests/PHPStan/Analyser/nsrt/constant-array-optional-set.php @@ -60,13 +60,13 @@ class Bar */ public function doFoo($nextAutoIndexes) { - assertType('non-empty-list|int', $nextAutoIndexes); + assertType('int|non-empty-list', $nextAutoIndexes); if (is_int($nextAutoIndexes)) { assertType('int', $nextAutoIndexes); } else { assertType('non-empty-list', $nextAutoIndexes); } - assertType('non-empty-list|int', $nextAutoIndexes); + assertType('int|non-empty-list', $nextAutoIndexes); } /** @@ -75,7 +75,7 @@ public function doFoo($nextAutoIndexes) */ public function doBar($nextAutoIndexes) { - assertType('non-empty-list|int', $nextAutoIndexes); + assertType('int|non-empty-list', $nextAutoIndexes); if (is_int($nextAutoIndexes)) { $nextAutoIndexes = [$nextAutoIndexes]; assertType('array{int}', $nextAutoIndexes); diff --git a/tests/PHPStan/Analyser/nsrt/constant-string-unions.php b/tests/PHPStan/Analyser/nsrt/constant-string-unions.php index 06881808e8..b97b117179 100644 --- a/tests/PHPStan/Analyser/nsrt/constant-string-unions.php +++ b/tests/PHPStan/Analyser/nsrt/constant-string-unions.php @@ -72,7 +72,7 @@ public function testLimit(string $s15, string $s16, string $s17) { // union should contain 32 elements assertType("'1'|'10'|'10a'|'11'|'11a'|'12'|'12a'|'13'|'13a'|'14'|'14a'|'15'|'15a'|'16'|'16a'|'1a'|'2'|'2a'|'3'|'3a'|'4'|'4a'|'5'|'5a'|'6'|'6a'|'7'|'7a'|'8'|'8a'|'9'|'9a'", $s16); // fallback to the more general form - assertType("literal-string&non-falsy-string", $s17); + assertType("literal-string&lowercase-string&non-falsy-string", $s17); $left = rand() ? 'a' : 'b'; $right = rand() ? 'x' : 'y'; $left .= $right; @@ -80,7 +80,7 @@ public function testLimit(string $s15, string $s16, string $s17) { $left .= $right; assertType("'axxx'|'axxy'|'axyx'|'axyy'|'ayxx'|'ayxy'|'ayyx'|'ayyy'|'bxxx'|'bxxy'|'bxyx'|'bxyy'|'byxx'|'byxy'|'byyx'|'byyy'", $left); $left .= $right; - assertType("literal-string&non-falsy-string", $left); + assertType("literal-string&lowercase-string&non-falsy-string", $left); $left = rand() ? 'a' : 'b'; $right = rand() ? 'x' : 'y'; @@ -89,7 +89,7 @@ public function testLimit(string $s15, string $s16, string $s17) { $left = "{$left}{$right}"; assertType("'axxx'|'axxy'|'axyx'|'axyy'|'ayxx'|'ayxy'|'ayyx'|'ayyy'|'bxxx'|'bxxy'|'bxyx'|'bxyy'|'byxx'|'byxy'|'byyx'|'byyy'", $left); $left = "{$left}{$right}"; - assertType("literal-string&non-falsy-string", $left); + assertType("literal-string&lowercase-string&non-falsy-string", $left); } /** 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 index 255c936d22..4be30d9f49 100644 --- a/tests/PHPStan/Analyser/nsrt/count-maybe.php +++ b/tests/PHPStan/Analyser/nsrt/count-maybe.php @@ -86,9 +86,9 @@ function doFoo4($maybeCountable, int $mode): void if (count($maybeCountable, $mode) > 0) { assertType('non-empty-list', $maybeCountable); } else { - assertType('list|float', $maybeCountable); + assertType('float|list', $maybeCountable); } - assertType('list|float', $maybeCountable); + assertType('float|list', $maybeCountable); } /** @@ -100,9 +100,9 @@ function doFoo5($maybeCountable, $maybeMode): void if (count($maybeCountable, $maybeMode) > 0) { assertType('non-empty-list', $maybeCountable); } else { - assertType('list|float', $maybeCountable); + assertType('float|list', $maybeCountable); } - assertType('list|float', $maybeCountable); + assertType('float|list', $maybeCountable); } /** @@ -113,9 +113,9 @@ function doFoo6($maybeCountable, float $invalidMode): void if (count($maybeCountable, $invalidMode) > 0) { assertType('non-empty-list', $maybeCountable); } else { - assertType('list|float', $maybeCountable); + assertType('float|list', $maybeCountable); } - assertType('list|float', $maybeCountable); + assertType('float|list', $maybeCountable); } /** @@ -124,11 +124,11 @@ function doFoo6($maybeCountable, float $invalidMode): void function doFoo7($maybeCountable, int $mode): void { if (count($maybeCountable, $mode) > 0) { - assertType('non-empty-list|Countable', $maybeCountable); + assertType('Countable|non-empty-list', $maybeCountable); } else { - assertType('list|Countable|float', $maybeCountable); + assertType('Countable|float|list', $maybeCountable); } - assertType('list|Countable|float', $maybeCountable); + assertType('Countable|float|list', $maybeCountable); } /** @@ -138,11 +138,11 @@ function doFoo7($maybeCountable, int $mode): void function doFoo8($maybeCountable, $maybeMode): void { if (count($maybeCountable, $maybeMode) > 0) { - assertType('non-empty-list|Countable', $maybeCountable); + assertType('Countable|non-empty-list', $maybeCountable); } else { - assertType('list|Countable|float', $maybeCountable); + assertType('Countable|float|list', $maybeCountable); } - assertType('list|Countable|float', $maybeCountable); + assertType('Countable|float|list', $maybeCountable); } /** @@ -151,11 +151,11 @@ function doFoo8($maybeCountable, $maybeMode): void function doFoo9($maybeCountable, float $invalidMode): void { if (count($maybeCountable, $invalidMode) > 0) { - assertType('non-empty-list|Countable', $maybeCountable); + assertType('Countable|non-empty-list', $maybeCountable); } else { - assertType('list|Countable|float', $maybeCountable); + assertType('Countable|float|list', $maybeCountable); } - assertType('list|Countable|float', $maybeCountable); + assertType('Countable|float|list', $maybeCountable); } function doFooBar1(array $countable, int $mode): void diff --git a/tests/PHPStan/Analyser/nsrt/count-type.php b/tests/PHPStan/Analyser/nsrt/count-type.php index 58dadbdd4a..1deb2e8695 100644 --- a/tests/PHPStan/Analyser/nsrt/count-type.php +++ b/tests/PHPStan/Analyser/nsrt/count-type.php @@ -18,4 +18,104 @@ public function doFoo( 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 index 740430a4c5..1399228135 100644 --- a/tests/PHPStan/Analyser/nsrt/countable.php +++ b/tests/PHPStan/Analyser/nsrt/countable.php @@ -15,9 +15,38 @@ static public function doFoo() { } } +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())); - assertType('*ERROR*', count(new NonCountable())); +} + +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 index 835ba4fdcc..ed4704daa7 100644 --- a/tests/PHPStan/Analyser/nsrt/ctype-digit.php +++ b/tests/PHPStan/Analyser/nsrt/ctype-digit.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types=1); namespace CtypeDigit; @@ -20,7 +22,7 @@ public function foo(mixed $foo): void 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); + assertType('mixed~(int<48, 57>|int<256, max>)', $foo); } if (ctype_digit($foo)) { @@ -28,6 +30,6 @@ public function foo(mixed $foo): void return; } - assertType('mixed~int<48, 57>|int<256, max>', $foo); // not all numeric strings are covered by ctype_digit + 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/dynamic-sprintf.php b/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php index ec25be47cd..3555613fe0 100644 --- a/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php +++ b/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php @@ -33,7 +33,7 @@ public function integerRange(int $a, string $b): void */ public function tooBigRange(int $a, string $b): void { - assertType("non-falsy-string", sprintf('%d %s', $a, $b)); + assertType("lowercase-string&non-falsy-string", sprintf('%d %s', $a, $b)); } } 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.php b/tests/PHPStan/Analyser/nsrt/enum-reflection.php index 38f023a637..85e98f4f8d 100644 --- a/tests/PHPStan/Analyser/nsrt/enum-reflection.php +++ b/tests/PHPStan/Analyser/nsrt/enum-reflection.php @@ -41,12 +41,3 @@ public function doFoo(): void } } - -/** @param class-string $class */ -function testNarrowGetNameTypeAfterIsBacked(string $class) { - $r = new ReflectionEnum($class); - assertType('class-string', $r->getName()); - if ($r->isBacked()) { - assertType('class-string', $r->getName()); - } -} diff --git a/tests/PHPStan/Analyser/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 index 33f1200924..37809016ad 100644 --- a/tests/PHPStan/Analyser/nsrt/enum_exists.php +++ b/tests/PHPStan/Analyser/nsrt/enum_exists.php @@ -1,4 +1,4 @@ -= 8.0 namespace EnumExists; 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 index aad0f3ef5b..e91a274257 100644 --- a/tests/PHPStan/Analyser/nsrt/equal.php +++ b/tests/PHPStan/Analyser/nsrt/equal.php @@ -135,7 +135,7 @@ public static function createStdClass(): \stdClass class Baz { - public function doFoo(string $a, int $b, float $c): void + public function doFoo(string $a, float $c): void { $nullableA = $a; if (rand(0, 1)) { @@ -152,7 +152,6 @@ public function doFoo(string $a, int $b, float $c): void assertType('false', 'a' != 'a'); assertType('true', 'a' != 'b'); - assertType('bool', $b == 'a'); assertType('bool', $a == 1); assertType('true', 1 == 1); assertType('false', 1 == 0); diff --git a/tests/PHPStan/Analyser/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/nsrt/extract.php b/tests/PHPStan/Analyser/nsrt/extract.php index c57f2ef46d..dff42bc482 100644 --- a/tests/PHPStan/Analyser/nsrt/extract.php +++ b/tests/PHPStan/Analyser/nsrt/extract.php @@ -66,3 +66,21 @@ function doTyped3(array $vars): void assertVariableCertainty(TrinaryLogic::createNo(), $none); } + +function doTyped4(): void +{ + extract(['foo' => 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-isset-certainty.php b/tests/PHPStan/Analyser/nsrt/falsey-isset-certainty.php index 1fbb9547ac..484d0363e3 100644 --- a/tests/PHPStan/Analyser/nsrt/falsey-isset-certainty.php +++ b/tests/PHPStan/Analyser/nsrt/falsey-isset-certainty.php @@ -1,4 +1,4 @@ -= 8.0 namespace FalseyIssetCertainty; diff --git a/tests/PHPStan/Analyser/nsrt/falsey-ternary-certainty.php b/tests/PHPStan/Analyser/nsrt/falsey-ternary-certainty.php index 01045e25f9..cc831b87a4 100644 --- a/tests/PHPStan/Analyser/nsrt/falsey-ternary-certainty.php +++ b/tests/PHPStan/Analyser/nsrt/falsey-ternary-certainty.php @@ -1,4 +1,4 @@ -= 8.0 namespace FalseyTernaryCertainty; diff --git a/tests/PHPStan/Analyser/nsrt/falsy-isset.php b/tests/PHPStan/Analyser/nsrt/falsy-isset.php index bce229826a..eb11c5254d 100644 --- a/tests/PHPStan/Analyser/nsrt/falsy-isset.php +++ b/tests/PHPStan/Analyser/nsrt/falsy-isset.php @@ -1,4 +1,4 @@ -= 8.0 namespace FalsyIsset; diff --git a/tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php b/tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php new file mode 100644 index 0000000000..8a38465040 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php @@ -0,0 +1,12 @@ +|false|null', fgetcsv($resource)); // nullable when invalid argument is given (https://3v4l.org/4WmR5#v7.4.30) +} diff --git a/tests/PHPStan/Analyser/nsrt/fgetcsv-php8.php b/tests/PHPStan/Analyser/nsrt/fgetcsv-php8.php new file mode 100644 index 0000000000..fccf29931c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/fgetcsv-php8.php @@ -0,0 +1,12 @@ += 8.0 + +declare(strict_types = 1); + +namespace TestFGetCsvPhp8; + +use function PHPStan\Testing\assertType; + +function test($resource): void +{ + assertType('list|false', fgetcsv($resource)); +} diff --git a/tests/PHPStan/Analyser/nsrt/filesystem-functions.php b/tests/PHPStan/Analyser/nsrt/filesystem-functions.php index 99dbd676f1..fc7614c63a 100644 --- a/tests/PHPStan/Analyser/nsrt/filesystem-functions.php +++ b/tests/PHPStan/Analyser/nsrt/filesystem-functions.php @@ -1,4 +1,4 @@ -= 7.4 += 8.0 + +declare(strict_types=1); namespace FilterVarArray; diff --git a/tests/PHPStan/Analyser/nsrt/filter-var-array.php b/tests/PHPStan/Analyser/nsrt/filter-var-array.php index 52261b0e8a..1151d370c1 100644 --- a/tests/PHPStan/Analyser/nsrt/filter-var-array.php +++ b/tests/PHPStan/Analyser/nsrt/filter-var-array.php @@ -1,4 +1,4 @@ -= 8.0 namespace FilterVarArray; @@ -85,11 +85,11 @@ function mixedInput(mixed $input): void ], false)); // filter flag with add_empty=default - assertType('array', filter_var_array($input, FILTER_VALIDATE_INT)); + 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)); + 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)); + assertType('array', filter_var_array($input, FILTER_VALIDATE_INT, false)); $filter = [ 'filter' => FILTER_VALIDATE_INT, 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 index 69fc99f613..172fa40b4f 100644 --- 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 @@ -46,9 +46,9 @@ public function test() } assertType('array{default?: PHPStan\Type\Type, range: PHPStan\Type\Type}', $otherTypes); } - assertType('array{default?: PHPStan\Type\Type, range?: PHPStan\Type\Type}&non-empty-array', $otherTypes); + assertType('non-empty-array{default?: PHPStan\Type\Type, range?: PHPStan\Type\Type}', $otherTypes); if ($exactType !== null) { - assertType('array{default?: PHPStan\Type\Type, range?: PHPStan\Type\Type}&non-empty-array', $otherTypes); + assertType('non-empty-array{default?: PHPStan\Type\Type, range?: PHPStan\Type\Type}', $otherTypes); unset($otherTypes['default']); assertType('array{range?: PHPStan\Type\Type}', $otherTypes); } diff --git a/tests/PHPStan/Analyser/nsrt/filter-var.php b/tests/PHPStan/Analyser/nsrt/filter-var.php index 12f97e63b5..abe5331fc6 100644 --- a/tests/PHPStan/Analyser/nsrt/filter-var.php +++ b/tests/PHPStan/Analyser/nsrt/filter-var.php @@ -106,12 +106,14 @@ public function scalars(bool $bool, float $float, int $int, string $string, int 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)); @@ -121,12 +123,14 @@ public function scalars(bool $bool, float $float, int $int, string $string, int 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)); @@ -136,27 +140,31 @@ public function scalars(bool $bool, float $float, int $int, string $string, 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', filter_var($float)); + assertType('numeric-string&uppercase-string', filter_var($float)); assertType("'17'", filter_var(17.0)); assertType("'17.1'", filter_var(17.1)); - assertType('numeric-string', filter_var($int)); - assertType('numeric-string', filter_var($intRange)); + 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/for-loop-i-type.php b/tests/PHPStan/Analyser/nsrt/for-loop-i-type.php index 011d15d6c7..1317b3695c 100644 --- a/tests/PHPStan/Analyser/nsrt/for-loop-i-type.php +++ b/tests/PHPStan/Analyser/nsrt/for-loop-i-type.php @@ -94,4 +94,12 @@ public static function groupCapacities(array $startTimes): array return $capacities; } + + public function lastConditionResult(): void + { + for ($i = 0, $j = 5; $i < 10, $j > 0; $i++, $j--) { + assertType('int<0, max>', $i); // int<0,4> would be more precise, see https://github.com/phpstan/phpstan/issues/11872 + assertType('int<1, 5>', $j); + } + } } diff --git a/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php b/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php index ad8e375e58..a1d7279252 100644 --- a/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php +++ b/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php @@ -29,7 +29,7 @@ public function sayHello(\stdClass $s): void foreach ($s as $k => $v) { $a .= 'test'; } - assertType('(literal-string&non-falsy-string)|null', $a); + assertType('(literal-string&lowercase-string&non-falsy-string)|null', $a); } } diff --git a/tests/PHPStan/Analyser/nsrt/generic-callables.php b/tests/PHPStan/Analyser/nsrt/generic-callables.php index 94bb3238a2..9fde822894 100644 --- a/tests/PHPStan/Analyser/nsrt/generic-callables.php +++ b/tests/PHPStan/Analyser/nsrt/generic-callables.php @@ -1,4 +1,4 @@ -= 8.0 namespace GenericCallables; diff --git a/tests/PHPStan/Analyser/nsrt/generic-method-tags.php b/tests/PHPStan/Analyser/nsrt/generic-method-tags.php index 92fdfaef5c..0aab6ea591 100644 --- a/tests/PHPStan/Analyser/nsrt/generic-method-tags.php +++ b/tests/PHPStan/Analyser/nsrt/generic-method-tags.php @@ -1,4 +1,4 @@ -= 8.0 namespace GenericMethodTags; diff --git a/tests/PHPStan/Analyser/nsrt/generic-static.php b/tests/PHPStan/Analyser/nsrt/generic-static.php new file mode 100644 index 0000000000..c7163151f7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-static.php @@ -0,0 +1,173 @@ + + */ + public function map(callable $cb); + + /** @return static */ + public function flip(); + + /** @return static */ + public function fluent(); + + /** @return static> */ + public function nested(); + +} + +/** + * @template T + * @template U + * @implements Foo + */ +class FooImpl implements Foo +{ + + public function map(callable $cb) + { + + } + + public function flip() + { + + } + + public function fluent() + { + + } + + public function doFoo(): void + { + assertType('static(GenericStatic\FooImpl)', $this->map(function () { + return 1; + })); + + assertType('static(GenericStatic\FooImpl)', $this->flip()); + assertType('static(GenericStatic\FooImpl)', $this->fluent()); + assertType('static(GenericStatic\FooImpl)>)', $this->nested()); + } + + /** + * @param FooImpl $s + */ + public function doBar(self $s): void + { + assertType('GenericStatic\\FooImpl', $s->map(function () { + return 1; + })); + + assertType('GenericStatic\\FooImpl', $s->flip()); + assertType('GenericStatic\\FooImpl', $s->fluent()); + assertType('GenericStatic\FooImpl>', $s->nested()); + } + +} + +/** + * @template T + * @template U + * @implements Foo + */ +abstract class Inconsistent implements Foo +{ + + public function fluent() + { + + } + + /** + * @param Inconsistent $s + */ + public function test(self $s): void + { + assertType('static(GenericStatic\Inconsistent)', $this->fluent()); + assertType('GenericStatic\\Inconsistent', $s->fluent()); + } + +} + +/** + * @template T + * @implements Foo + */ +abstract class Inconsistent2 implements Foo +{ + + public function fluent() + { + + } + + /** + * @param Inconsistent2 $s + */ + public function test(self $s): void + { + assertType('static(GenericStatic\Inconsistent2)', $this->fluent()); + assertType('GenericStatic\\Inconsistent2', $s->fluent()); + } + +} + +/** + * @template T + * @implements Foo + */ +abstract class Inconsistent3 implements Foo +{ + + public function fluent() + { + + } + + /** + * @param Inconsistent3 $s + */ + public function test(self $s): void + { + assertType('static(GenericStatic\Inconsistent3)', $this->fluent()); + assertType('GenericStatic\\Inconsistent3', $s->fluent()); + } + +} + +/** + * @template T + * @template K + */ +class A { + /** @return static */ + public function doFoo() {} + +} + +/** @extends A */ +class B extends A { + public function doBar(): void + { + $f = $this->doFoo(); + assertType('static(GenericStatic\B)', $f); + } +} + +function (): void { + assertType(B::class, (new B)->doFoo()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/generics.php b/tests/PHPStan/Analyser/nsrt/generics.php index bcd7ecf616..1968fab471 100644 --- a/tests/PHPStan/Analyser/nsrt/generics.php +++ b/tests/PHPStan/Analyser/nsrt/generics.php @@ -147,10 +147,10 @@ function f($a, $b) */ function testF($arrayOfInt, $callableOrNull) { - assertType('Closure(int): numeric-string', function (int $a): string { + assertType('Closure(int): (lowercase-string&numeric-string&uppercase-string)', function (int $a): string { return (string)$a; }); - assertType('array', f($arrayOfInt, function (int $a): string { + assertType('array', f($arrayOfInt, function (int $a): string { return (string)$a; })); assertType('Closure(mixed): string', function ($a): string { @@ -163,8 +163,8 @@ function testF($arrayOfInt, $callableOrNull) return $a; })); assertType('array', f($arrayOfInt, $callableOrNull)); - assertType('array', f($arrayOfInt, null)); - assertType('array', f($arrayOfInt, '')); + assertType('array', f($arrayOfInt, null)); + assertType('array', f($arrayOfInt, '')); } /** @@ -224,7 +224,7 @@ function testArrayMap(array $listOfIntegers) return (string) $int; }, $listOfIntegers); - assertType('array', $strings); + assertType('array', $strings); } /** @@ -741,7 +741,7 @@ function testClasses() assertType('DateTime', $ab->getB(new \DateTime())); $noConstructor = new NoConstructor(1); - assertType('PHPStan\Generics\FunctionsAssertType\NoConstructor', $noConstructor); + assertType('PHPStan\Generics\FunctionsAssertType\NoConstructor', $noConstructor); assertType('stdClass', acceptsClassString(\stdClass::class)); assertType('class-string', returnsClassString(new \stdClass())); diff --git a/tests/PHPStan/Analyser/nsrt/get-debug-type.php b/tests/PHPStan/Analyser/nsrt/get-debug-type.php index 975ea62613..85c4d02b47 100644 --- a/tests/PHPStan/Analyser/nsrt/get-debug-type.php +++ b/tests/PHPStan/Analyser/nsrt/get-debug-type.php @@ -5,6 +5,9 @@ use function PHPStan\Testing\assertType; final class A {} +interface B {} +interface C {} +class D {} /** * @param double $d @@ -12,12 +15,15 @@ final class A {} * @param int|string $intOrString * @param array|A $arrayOrObject */ -function doFoo(bool $b, int $i, float $f, $d, $r, string $s, array $a, $intOrString, $arrayOrObject) { +function doFoo(bool $b, int $i, float $f, $d, $r, string $s, array $a, $intOrString, $arrayOrObject, \stdClass $std) { $null = null; $resource = fopen('php://memory', 'r'); $o = new \stdClass(); $A = new A(); $anonymous = new class {}; + $anonymousImplements = new class implements B, C {}; + $anonymousExtends = new class extends D {}; + $anonymousExtendsImplements = new class extends D implements B, C {}; assertType("'bool'", get_debug_type($b)); assertType("'bool'", get_debug_type(true)); @@ -27,14 +33,18 @@ function doFoo(bool $b, int $i, float $f, $d, $r, string $s, array $a, $intOrStr assertType("'float'", get_debug_type($d)); assertType("'string'", get_debug_type($s)); assertType("'array'", get_debug_type($a)); - assertType("string", get_debug_type($o)); + assertType("'stdClass'", get_debug_type($o)); + assertType("string", get_debug_type($std)); assertType("'GetDebugType\\\\A'", get_debug_type($A)); assertType("string", get_debug_type($r)); - assertType("'bool'|string", get_debug_type($resource)); + assertType("string", get_debug_type($resource)); assertType("'null'", get_debug_type($null)); assertType("'int'|'string'", get_debug_type($intOrString)); assertType("'array'|'GetDebugType\\\\A'", get_debug_type($arrayOrObject)); assertType("'class@anonymous'", get_debug_type($anonymous)); + assertType("'GetDebugType\\\\B@anonymous'", get_debug_type($anonymousImplements)); + assertType("'GetDebugType\\\\D@anonymous'", get_debug_type($anonymousExtends)); + assertType("'GetDebugType\\\\D@anonymous'", get_debug_type($anonymousExtendsImplements)); } /** diff --git a/tests/PHPStan/Analyser/nsrt/get-defined-vars.php b/tests/PHPStan/Analyser/nsrt/get-defined-vars.php new file mode 100644 index 0000000000..345d54dbd3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/get-defined-vars.php @@ -0,0 +1,46 @@ +', 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/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 index 453667cd51..aae0e128e5 100644 --- a/tests/PHPStan/Analyser/nsrt/getopt.php +++ b/tests/PHPStan/Analyser/nsrt/getopt.php @@ -5,5 +5,6 @@ use function getopt; use function PHPStan\Testing\assertType; -$opts = getopt("ab:c::", ["longopt1", "longopt2:", "longopt3::"]); +$opts = getopt("ab:c::", ["longopt1", "longopt2:", "longopt3::"], $restIndex); assertType('(array|string|false>|false)', $opts); +assertType('int<1, max>', $restIndex); diff --git a/tests/PHPStan/Analyser/nsrt/globals.php b/tests/PHPStan/Analyser/nsrt/globals.php index 7b7b7b8a01..da2ae35787 100644 --- a/tests/PHPStan/Analyser/nsrt/globals.php +++ b/tests/PHPStan/Analyser/nsrt/globals.php @@ -1,11 +1,11 @@ ', $GLOBALS); +\PHPStan\Testing\assertType('array', $_SERVER); +\PHPStan\Testing\assertType('array', $_GET); +\PHPStan\Testing\assertType('array', $_POST); +\PHPStan\Testing\assertType('array', $_FILES); +\PHPStan\Testing\assertType('array', $_COOKIE); +\PHPStan\Testing\assertType('array', $_SESSION); +\PHPStan\Testing\assertType('array', $_REQUEST); +\PHPStan\Testing\assertType('array', $_ENV); diff --git a/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php b/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php index 8b470ba316..eacfb06af6 100644 --- a/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php +++ b/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php @@ -44,14 +44,14 @@ public function doFoo(array $errorMessages): void */ public function doBar(array $result): void { - assertType('array', $result); + assertType('array', $result); assert($result['totals']['file_errors'] === 3); - assertType("array", $result); + 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("array", $result); assertType("mixed", $result['totals']); assertType('3', $result['totals']['file_errors']); assertType('0', $result['totals']['errors']); @@ -65,7 +65,7 @@ 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("array{min?: bool|float|int|string|null, max?: bool|float|int|string|null}&non-empty-array", $range); + 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); } @@ -144,7 +144,7 @@ public function doBar(array $a, int $i) public function doFoo2(array $a) { if (is_int($a['a'])) { - assertType("array&hasOffsetValue('a', *NEVER*)", $a); + assertType("non-empty-array&hasOffsetValue('a', *NEVER*)", $a); } } diff --git a/tests/PHPStan/Analyser/nsrt/hash-functions-74.php b/tests/PHPStan/Analyser/nsrt/hash-functions-74.php index 0d068d3765..2ffcb920f8 100644 --- a/tests/PHPStan/Analyser/nsrt/hash-functions-74.php +++ b/tests/PHPStan/Analyser/nsrt/hash-functions-74.php @@ -11,7 +11,8 @@ public function hash_hmac(string $string): void { assertType('false', hash_hmac('crc32', 'data', 'key')); assertType('false', hash_hmac('invalid', 'data', 'key')); - assertType('(non-empty-string|false)', hash_hmac($string, 'data', 'key')); + assertType('((lowercase-string&non-falsy-string)|false)', hash_hmac($string, 'data', 'key')); + assertType('(non-falsy-string|false)', hash_hmac($string, 'data', 'key', true)); } public function hash_hmac_file(): void @@ -23,7 +24,8 @@ public function hash_hmac_file(): void public function hash(string $string): void { assertType('false', hash('invalid', 'data', false)); - assertType('(non-empty-string|false)', hash($string, 'data')); + assertType('((lowercase-string&non-falsy-string)|false)', hash($string, 'data')); + assertType('(non-falsy-string|false)', hash($string, 'data', true)); } public function hash_file(): void @@ -35,14 +37,15 @@ public function hash_hkdf(string $string): void { assertType('false', hash_hkdf('crc32', 'key')); assertType('false', hash_hkdf('invalid', 'key')); - assertType('(non-empty-string|false)', hash_hkdf($string, 'key')); + assertType('(non-falsy-string|false)', hash_hkdf($string, 'key')); } public function hash_pbkdf2(string $string): void { assertType('false', hash_pbkdf2('crc32', 'password', 'salt', 1000)); assertType('false', hash_pbkdf2('invalid', 'password', 'salt', 1000)); - assertType('(non-empty-string|false)', hash_pbkdf2($string, 'password', 'salt', 1000)); + assertType('((lowercase-string&non-falsy-string)|false)', hash_pbkdf2($string, 'password', 'salt', 1000)); + assertType('(non-falsy-string|false)', hash_pbkdf2($string, 'password', 'salt', 1000, 0, true)); } public function caseSensitive() diff --git a/tests/PHPStan/Analyser/nsrt/hash-functions-80.php b/tests/PHPStan/Analyser/nsrt/hash-functions-80.php index fe8cd726f0..1d11247d53 100644 --- a/tests/PHPStan/Analyser/nsrt/hash-functions-80.php +++ b/tests/PHPStan/Analyser/nsrt/hash-functions-80.php @@ -11,7 +11,8 @@ public function hash_hmac(string $string): void { assertType('*NEVER*', hash_hmac('crc32', 'data', 'key')); assertType('*NEVER*', hash_hmac('invalid', 'data', 'key')); - assertType('non-empty-string', hash_hmac($string, '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 @@ -23,7 +24,7 @@ public function hash_hmac_file(): void public function hash(string $string): void { assertType('*NEVER*', hash('invalid', 'data', false)); - assertType('non-falsy-string', hash($string, 'data')); + assertType('lowercase-string&non-falsy-string', hash($string, 'data')); } public function hash_file(): void @@ -42,7 +43,7 @@ public function hash_pbkdf2(string $string): void { assertType('*NEVER*', hash_pbkdf2('crc32', 'password', 'salt', 1000)); assertType('*NEVER*', hash_pbkdf2('invalid', 'password', 'salt', 1000)); - assertType('non-empty-string', hash_pbkdf2($string, 'password', 'salt', 1000)); + assertType('lowercase-string&non-falsy-string', hash_pbkdf2($string, 'password', 'salt', 1000)); } public function caseSensitive() diff --git a/tests/PHPStan/Analyser/nsrt/hash-functions.php b/tests/PHPStan/Analyser/nsrt/hash-functions.php index 17b9eb4d8c..72f977baae 100644 --- a/tests/PHPStan/Analyser/nsrt/hash-functions.php +++ b/tests/PHPStan/Analyser/nsrt/hash-functions.php @@ -13,44 +13,52 @@ class HashFunctionTests public function hash_hmac(): void { - assertType('non-empty-string', hash_hmac('md5', 'data', 'key')); - assertType('non-empty-string', hash_hmac('sha256', 'data', 'key')); + assertType('lowercase-string&non-falsy-string', hash_hmac('md5', 'data', 'key')); + assertType('non-falsy-string', hash_hmac('md5', 'data', 'key', true)); + assertType('lowercase-string&non-falsy-string', hash_hmac('sha256', 'data', 'key')); + assertType('non-falsy-string', hash_hmac('sha256', 'data', 'key', true)); } public function hash_hmac_file(string $string): void { - assertType('non-empty-string|false', hash_hmac_file('md5', 'filename', 'key')); - assertType('non-empty-string|false', hash_hmac_file('sha256', 'filename', 'key')); - assertType('(non-empty-string|false)', hash_hmac_file($string, 'filename', 'key')); + assertType('(lowercase-string&non-falsy-string)|false', hash_hmac_file('md5', 'filename', 'key')); + assertType('non-falsy-string|false', hash_hmac_file('md5', 'filename', 'key', true)); + assertType('(lowercase-string&non-falsy-string)|false', hash_hmac_file('sha256', 'filename', 'key')); + assertType('non-falsy-string|false', hash_hmac_file('sha256', 'filename', 'key', true)); + assertType('((lowercase-string&non-falsy-string)|false)', hash_hmac_file($string, 'filename', 'key')); + assertType('(non-falsy-string|false)', hash_hmac_file($string, 'filename', 'key', true)); } public function hash($mixed): void { - assertType('non-empty-string', hash('sha256', 'data', false)); - assertType('non-empty-string', hash('sha256', 'data', true)); - assertType('non-empty-string', hash('md5', $mixed, false)); + assertType('lowercase-string&non-falsy-string', hash('sha256', 'data', false)); + assertType('non-falsy-string', hash('sha256', 'data', true)); + assertType('lowercase-string&non-falsy-string', hash('md5', $mixed, false)); } public function hash_file(): void { - assertType('non-empty-string|false', hash_file('sha256', 'filename', false)); - assertType('non-empty-string|false', hash_file('sha256', 'filename', true)); - assertType('non-empty-string|false', hash_file('crc32', 'filename')); + assertType('(lowercase-string&non-falsy-string)|false', hash_file('sha256', 'filename', false)); + assertType('non-falsy-string|false', hash_file('sha256', 'filename', true)); + assertType('(lowercase-string&non-falsy-string)|false', hash_file('crc32', 'filename')); + assertType('non-falsy-string|false', hash_file('crc32', 'filename', true)); } public function hash_hkdf(): void { - assertType('non-empty-string', hash_hkdf('sha256', 'key')); + assertType('non-falsy-string', hash_hkdf('sha256', 'key')); } public function hash_pbkdf2(): void { - assertType('non-empty-string', hash_pbkdf2('sha256', 'password', 'salt', 1000)); + assertType('lowercase-string&non-falsy-string', hash_pbkdf2('sha256', 'password', 'salt', 1000)); + assertType('non-falsy-string', hash_pbkdf2('sha256', 'password', 'salt', 1000, 0, true)); } public function caseSensitive() { - assertType('non-empty-string', hash('SHA256', 'data')); + assertType('lowercase-string&non-falsy-string', hash('SHA256', 'data')); + assertType('non-falsy-string', hash('SHA256', 'data', true)); } public function constantStrings(int $type) @@ -69,7 +77,8 @@ public function constantStrings(int $type) return; } - assertType('non-empty-string', hash_pbkdf2($algorithm, 'password', 'salt', 1000)); + assertType('lowercase-string&non-falsy-string', hash_pbkdf2($algorithm, 'password', 'salt', 1000)); + assertType('non-falsy-string', hash_pbkdf2($algorithm, 'password', 'salt', 1000, 0, true)); } } diff --git a/tests/PHPStan/Analyser/nsrt/implode.php b/tests/PHPStan/Analyser/nsrt/implode.php index b2e2b9aa7c..8e97e19f72 100644 --- a/tests/PHPStan/Analyser/nsrt/implode.php +++ b/tests/PHPStan/Analyser/nsrt/implode.php @@ -6,6 +6,18 @@ class Foo { + /** + * @param array $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; @@ -21,4 +33,39 @@ public function constants() { 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/in_array_loose.php b/tests/PHPStan/Analyser/nsrt/in_array_loose.php index 4600ae0a13..78d2899b8c 100644 --- a/tests/PHPStan/Analyser/nsrt/in_array_loose.php +++ b/tests/PHPStan/Analyser/nsrt/in_array_loose.php @@ -1,4 +1,4 @@ -= 8.0 namespace InArrayLoose; 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/nsrt/instanceof-class-string.php b/tests/PHPStan/Analyser/nsrt/instanceof-class-string.php index dc2bcaa2f3..22c58c4b20 100644 --- a/tests/PHPStan/Analyser/nsrt/instanceof-class-string.php +++ b/tests/PHPStan/Analyser/nsrt/instanceof-class-string.php @@ -32,7 +32,7 @@ public function doBar(Foo $foo, Bar $bar): void if ($foo instanceof $class) { assertType(self::class, $foo); } else { - assertType('InstanceOfClassString\Foo~InstanceOfClassString\Bar', $foo); + assertType('InstanceOfClassString\Foo', $foo); } } diff --git a/tests/PHPStan/Analyser/nsrt/instanceof.php b/tests/PHPStan/Analyser/nsrt/instanceof.php index 098b74cb47..fe111fb0e4 100644 --- a/tests/PHPStan/Analyser/nsrt/instanceof.php +++ b/tests/PHPStan/Analyser/nsrt/instanceof.php @@ -19,11 +19,10 @@ abstract class BarParent class Foo extends BarParent { - public function someMethod(Expr $foo) + public function someMethod(Expr $foo, Foo $intersected) { $bar = $foo; $baz = doFoo(); - $intersected = new Foo(); $parent = doFoo(); if ($baz instanceof Foo) { @@ -80,9 +79,9 @@ public function testExprInstanceof($subject, string $classString, $union, $inter assertType('true', $subject instanceof Foo); assertType('bool', $subject instanceof $classString); } else { - assertType('mixed~InstanceOfNamespace\Foo', $subject); - assertType('false', $subject instanceof Foo); - assertType('false', $subject instanceof $classString); + assertType('mixed', $subject); + assertType('bool', $subject instanceof Foo); + assertType('bool', $subject instanceof $classString); // could be false } $constantString = 'InstanceOfNamespace\BarParent'; @@ -132,7 +131,7 @@ public function testExprInstanceof($subject, string $classString, $union, $inter assertType('ObjectT of InstanceOfNamespace\BarInterface (method InstanceOfNamespace\Foo::testExprInstanceof(), argument)', $subject); assertType('bool', $subject instanceof $objectT); } else { - assertType('mixed~ObjectT of InstanceOfNamespace\BarInterface (method InstanceOfNamespace\Foo::testExprInstanceof(), argument)', $subject); + assertType('mixed', $subject); assertType('bool', $subject instanceof $objectT); // can be false } @@ -140,7 +139,7 @@ public function testExprInstanceof($subject, string $classString, $union, $inter assertType('ObjectT of InstanceOfNamespace\BarInterface (method InstanceOfNamespace\Foo::testExprInstanceof(), argument)', $subject); assertType('bool', $subject instanceof $objectTString); } else { - assertType('mixed~ObjectT of InstanceOfNamespace\BarInterface (method InstanceOfNamespace\Foo::testExprInstanceof(), argument)', $subject); + assertType('mixed', $subject); assertType('bool', $subject instanceof $objectTString); // can be false } @@ -148,7 +147,7 @@ public function testExprInstanceof($subject, string $classString, $union, $inter assertType('MixedT (method InstanceOfNamespace\Foo::testExprInstanceof(), argument)&object', $subject); assertType('bool', $subject instanceof $mixedTString); } else { - assertType('mixed~MixedT (method InstanceOfNamespace\Foo::testExprInstanceof(), argument)', $subject); + assertType('mixed', $subject); assertType('bool', $subject instanceof $mixedTString); // can be false } @@ -180,8 +179,8 @@ public function testExprInstanceof($subject, string $classString, $union, $inter assertType('InstanceOfNamespace\Foo', $object); assertType('bool', $object instanceof $classString); } else { - assertType('object~InstanceOfNamespace\Foo', $object); - assertType('false', $object instanceof $classString); + assertType('object', $object); + assertType('bool', $object instanceof $classString); // could be false } if ($instance instanceof $string) { diff --git a/tests/PHPStan/Analyser/nsrt/integer-range-types.php b/tests/PHPStan/Analyser/nsrt/integer-range-types.php index c769890c9e..876a791352 100644 --- a/tests/PHPStan/Analyser/nsrt/integer-range-types.php +++ b/tests/PHPStan/Analyser/nsrt/integer-range-types.php @@ -446,3 +446,44 @@ public function zeroIssues($positive, $negative) } } + +function subtract($m) { + if ($m != 0) { + assertType("mixed~(0|0.0|'0'|false|null)", $m); // could be "mixed~(0|0.0|''|'0'|false|null)" + assertType('int', (int) $m); + } + if ($m !== 0) { + assertType("mixed~0", $m); + assertType('int', (int) $m); // mixed could still contain falsey values, which cast to 0 + } + if (!is_int($m)) { + assertType("mixed~int", $m); + assertType('int', (int) $m); // mixed could still contain falsey values, which cast to 0 + } + + if ($m != true) { + assertType("0|0.0|''|'0'|array{}|false|null", $m); + assertType('0', (int) $m); + } + if ($m !== true) { + assertType("mixed~true", $m); + assertType('int', (int) $m); + } + + if ($m != false) { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $m); + assertType('int', (int) $m); + } + if ($m !== false) { + assertType("mixed~false", $m); + assertType('int', (int) $m); // mixed could still contain falsey values, which cast to 0 + } + if (!is_string($m) && !is_float($m)) { + assertType("mixed~(float|string)", $m); + assertType('int', (int) $m); + if ($m != false) { + assertType("mixed~(0|array{}|float|string|false|null)", $m); + assertType('int|int<1, max>', (int) $m); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php index ef291855ed..d62b67b220 100644 --- a/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php +++ b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php @@ -1,4 +1,4 @@ -= 7.4 +', rand() ?? false); - assertType('0|string', preg_replace('', '', '') ?? 0); + assertType('0|(lowercase-string&uppercase-string)', preg_replace('', '', '') ?? 0); $foo = new FooCoalesce(); diff --git a/tests/PHPStan/Analyser/nsrt/iterator_to_array.php b/tests/PHPStan/Analyser/nsrt/iterator_to_array.php index 64ecbdeb05..038b36f7fd 100644 --- a/tests/PHPStan/Analyser/nsrt/iterator_to_array.php +++ b/tests/PHPStan/Analyser/nsrt/iterator_to_array.php @@ -2,6 +2,7 @@ namespace IteratorToArray; +use stdClass; use Traversable; use function iterator_to_array; use function PHPStan\Testing\assertType; @@ -31,4 +32,36 @@ 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/key-exists.php b/tests/PHPStan/Analyser/nsrt/key-exists.php index 11c2ed6a2a..67c42f6c14 100644 --- a/tests/PHPStan/Analyser/nsrt/key-exists.php +++ b/tests/PHPStan/Analyser/nsrt/key-exists.php @@ -1,4 +1,4 @@ -= 8.0 namespace KeyExists; @@ -49,16 +49,16 @@ public function doBar(array $a, array $b, array $c, int $key1, string $key2, int assertType('int', $key1); } if (key_exists($key2, $a)) { - assertType('numeric-string', $key2); + assertType('lowercase-string&numeric-string&uppercase-string', $key2); } if (key_exists($key3, $a)) { - assertType('int|numeric-string', $key3); + assertType('int|(lowercase-string&numeric-string&uppercase-string)', $key3); } if (key_exists($key4, $a)) { - assertType('(int|numeric-string)', $key4); + assertType('(int|(lowercase-string&numeric-string&uppercase-string))', $key4); } if (key_exists($key5, $a)) { - assertType('int|numeric-string', $key5); + assertType('int|(lowercase-string&numeric-string&uppercase-string)', $key5); } if (key_exists($key1, $b)) { diff --git a/tests/PHPStan/Analyser/nsrt/list-count.php b/tests/PHPStan/Analyser/nsrt/list-count.php index caf0a17c87..b42f0ec8bd 100644 --- a/tests/PHPStan/Analyser/nsrt/list-count.php +++ b/tests/PHPStan/Analyser/nsrt/list-count.php @@ -341,35 +341,62 @@ protected function testOptionalKeysInUnionArray($row): void /** * @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, $twoOrThree, $twoOrMore, int $maxThree, $tenOrEleven): void + 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{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list)|array{string}', $row); + 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{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list)|array{string}', $row); + assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); } if (count($row) >= $twoOrMore) { - assertType('array{0: int, 1: string|null, 2?: int|null, 3?: float|null}&list', $row); + assertType('list{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}&list)|array{string}', $row); + assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); } if (count($row) >= $maxThree) { - assertType('(array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list)|array{string}', $row); + assertType('array{string}|list{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}&list', $row); + 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); } } @@ -379,11 +406,53 @@ protected function testOptionalKeysInUnionListWithIntRange($row, $twoOrThree, $t */ protected function testOptionalKeysInUnionArrayWithIntRange($row, $twoOrThree): void { - // doesn't narrow because no list if (count($row) >= $twoOrThree) { - assertType('array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + assertType('array{0: int, 1: string|null, 2?: int|null}', $row); } else { assertType('array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}|array{string}', $row); } } } + +class FooBug +{ + public int $totalExpectedRows = 0; + + /** @var list<\stdClass> */ + public array $importedDaySummaryRows = []; + + public function sayHello(): void + { + assertType('int', $this->totalExpectedRows); + assertType('list', $this->importedDaySummaryRows); + if ($this->totalExpectedRows !== count($this->importedDaySummaryRows)) { + assertType('int', $this->totalExpectedRows); + assertType('list', $this->importedDaySummaryRows); + } + assertType('int', $this->totalExpectedRows); + assertType('list', $this->importedDaySummaryRows); + } +} + +class FooBugPositiveInt +{ + /** + * @var positive-int + */ + public int $totalExpectedRows = 1; + + /** @var list<\stdClass> */ + public array $importedDaySummaryRows = []; + + public function sayHello(): void + { + assertType('int<1, max>', $this->totalExpectedRows); + assertType('list', $this->importedDaySummaryRows); + if ($this->totalExpectedRows !== count($this->importedDaySummaryRows)) { + assertType('int<1, max>', $this->totalExpectedRows); + assertType('list', $this->importedDaySummaryRows); + } + assertType('int<1, max>', $this->totalExpectedRows); + assertType('list', $this->importedDaySummaryRows); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/list-type.php b/tests/PHPStan/Analyser/nsrt/list-type.php index a80e8b066d..8101c1744b 100644 --- a/tests/PHPStan/Analyser/nsrt/list-type.php +++ b/tests/PHPStan/Analyser/nsrt/list-type.php @@ -9,19 +9,19 @@ class Foo /** @param list $list */ public function directAssertion($list): void { - assertType('list', $list); + assertType('list', $list); } /** @param list $list */ public function directAssertionParamHint(array $list): void { - assertType('list', $list); + assertType('list', $list); } /** @param list $list */ public function directAssertionNullableParamHint(array $list = null): void { - assertType('list|null', $list); + assertType('list|null', $list); } /** @param list<\DateTime> $list */ @@ -37,7 +37,7 @@ public function withoutGenerics(): void $list[] = '1'; $list[] = true; $list[] = new \stdClass(); - assertType('non-empty-list', $list); + assertType('non-empty-list', $list); } @@ -83,17 +83,17 @@ 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); + assertType('list', $list); $list[] = '1'; - assertType('non-empty-list', $list); + assertType('non-empty-list', $list); $list[] = '2'; - assertType('non-empty-list', $list); + assertType('non-empty-list', $list); unset($list[0]);//break list behaviour assertType('array, mixed>', $list); /** @var list $list2 */ $list2 = []; - assertType('list', $list2); + assertType('list', $list2); $list2[2] = '1';//Most likely to create a gap in indexes assertType('non-empty-array, mixed>&hasOffsetValue(2, \'1\')', $list2); } @@ -106,4 +106,67 @@ public function testUnset(array $list): void 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 index ff63036d9b..c30fbdac80 100644 --- a/tests/PHPStan/Analyser/nsrt/literal-string.php +++ b/tests/PHPStan/Analyser/nsrt/literal-string.php @@ -36,9 +36,10 @@ public function doFoo($literalString, string $string, $numericString) "'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'", str_repeat('a', 99) ); - assertType('literal-string&non-falsy-string', str_repeat('a', 100)); - assertType('literal-string&non-empty-string&numeric-string', str_repeat('0', 100)); // could be non-falsy-string - assertType('literal-string&non-falsy-string&numeric-string', str_repeat('1', 100)); + assertType('literal-string&lowercase-string&non-falsy-string', str_repeat('a', 100)); + assertType('literal-string&non-falsy-string&uppercase-string', str_repeat('A', 100)); + assertType('literal-string&lowercase-string&non-empty-string&numeric-string&uppercase-string', str_repeat('0', 100)); // could be non-falsy-string + assertType('literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string', str_repeat('1', 100)); // Repeating a numeric type multiple times can lead to a non-numeric type: 3v4l.org/aRBdZ assertType('non-empty-string', str_repeat($numericString, 100)); @@ -51,13 +52,14 @@ public function doFoo($literalString, string $string, $numericString) assertType("non-empty-string", str_repeat($numericString, 2)); assertType("literal-string", str_repeat($literalString, 1)); $x = rand(1,2); - assertType("literal-string&non-falsy-string", str_repeat(' 1 ', $x)); - assertType("literal-string&non-falsy-string", str_repeat('+1', $x)); - assertType("literal-string&non-falsy-string", str_repeat('1e9', $x)); - assertType("literal-string&non-falsy-string&numeric-string", str_repeat('19', $x)); + assertType("literal-string&lowercase-string&non-falsy-string&uppercase-string", str_repeat(' 1 ', $x)); + assertType("literal-string&lowercase-string&non-falsy-string&uppercase-string", str_repeat('+1', $x)); + assertType("literal-string&lowercase-string&non-falsy-string", str_repeat('1e9', $x)); + assertType("literal-string&non-falsy-string&uppercase-string", str_repeat('1E9', $x)); + assertType("literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string", str_repeat('19', $x)); $x = rand(0,2); - assertType("literal-string", str_repeat('19', $x)); + assertType("literal-string&lowercase-string&uppercase-string", str_repeat('19', $x)); $x = rand(-10,-1); assertType("*NEVER*", str_repeat('19', $x)); diff --git a/tests/PHPStan/Analyser/nsrt/loose-comparisons-php7.php b/tests/PHPStan/Analyser/nsrt/loose-comparisons-php7.php index d95fecab11..cc5b0f4eb4 100644 --- a/tests/PHPStan/Analyser/nsrt/loose-comparisons-php7.php +++ b/tests/PHPStan/Analyser/nsrt/loose-comparisons-php7.php @@ -46,4 +46,30 @@ public function sayEmptyStr( { assertType('true', $emptyStr == $zero); } + + /** + * @param 'php' $phpStr + * @param '' $emptyStr + */ + public function sayInt( + $emptyStr, + $phpStr, + int $int + ): void + { + assertType('bool', $int == $emptyStr); + assertType('bool', $int == $phpStr); + assertType('bool', $int == 'a'); + } + + /** + * @param "abc"|"def" $constNonFalsy + */ + public function sayConstUnion( + $constNonFalsy, + ): void + { + assertType('true', $constNonFalsy == 0); + assertType('true', "" == 0); + } } diff --git a/tests/PHPStan/Analyser/nsrt/loose-comparisons-php8.php b/tests/PHPStan/Analyser/nsrt/loose-comparisons-php8.php index bba8a89f20..3c12092eb7 100644 --- a/tests/PHPStan/Analyser/nsrt/loose-comparisons-php8.php +++ b/tests/PHPStan/Analyser/nsrt/loose-comparisons-php8.php @@ -46,4 +46,37 @@ public function sayEmptyStr( { 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 index 92bec36518..c385548cf5 100644 --- a/tests/PHPStan/Analyser/nsrt/loose-comparisons.php +++ b/tests/PHPStan/Analyser/nsrt/loose-comparisons.php @@ -526,6 +526,7 @@ public function sayEmptyArray( * @param array{} $emptyArr * @param 'php' $phpStr * @param '' $emptyStr + * @param non-falsy-string $nonFalsyString */ public function sayNonFalsyStr( $true, @@ -540,7 +541,8 @@ public function sayNonFalsyStr( $null, $emptyArr, $phpStr, - $emptyStr + $emptyStr, + $nonFalsyString ): void { assertType('true', $phpStr == $true); @@ -555,6 +557,100 @@ public function sayNonFalsyStr( assertType('false', $phpStr == $emptyArr); assertType('true', $phpStr == $phpStr); assertType('false', $phpStr == $emptyStr); + + assertType('bool', $nonFalsyString == $true); + assertType('false', $nonFalsyString == $false); + assertType('bool', $nonFalsyString == $one); + assertType('false', $nonFalsyString == $zero); + assertType('bool', $nonFalsyString == $minusOne); + assertType('bool', $nonFalsyString == $oneStr); + assertType('false', $nonFalsyString == $zeroStr); + assertType('bool', $nonFalsyString == $minusOneStr); + assertType('bool', $nonFalsyString == $plusOneStr); + assertType('false', $nonFalsyString == $null); + assertType('false', $nonFalsyString == $emptyArr); + assertType('bool', $nonFalsyString == $phpStr); + assertType('false', $nonFalsyString == $emptyStr); + } + + /** + * @param true $true + * @param false $false + * @param 1 $one + * @param 0 $zero + * @param -1 $minusOne + * @param '1' $oneStr + * @param '0' $zeroStr + * @param '-1' $minusOneStr + * @param '+1' $plusOneStr + * @param null $null + * @param array{} $emptyArr + * @param 'php' $phpStr + * @param '' $emptyStr + * @param numeric-string $numericStr + */ + public function sayStr( + $true, + $false, + $one, + $zero, + $minusOne, + $oneStr, + $zeroStr, + $minusOneStr, + $plusOneStr, + $null, + $emptyArr, + string $string, + $phpStr, + $emptyStr, + $numericStr, + ?string $stringOrNull, + ): void + { + assertType('bool', $string == $true); + assertType('bool', $string == $false); + assertType('bool', $string == $one); + assertType('bool', $string == $zero); + assertType('bool', $string == $minusOne); + assertType('bool', $string == $oneStr); + assertType('bool', $string == $zeroStr); + assertType('bool', $string == $minusOneStr); + assertType('bool', $string == $plusOneStr); + assertType('bool', $string == $null); + assertType('bool', $string == $stringOrNull); + assertType('false', $string == $emptyArr); + assertType('bool', $string == $phpStr); + assertType('bool', $string == $emptyStr); + assertType('bool', $string == $numericStr); + + assertType('bool', $numericStr == $true); + assertType('bool', $numericStr == $false); + assertType('bool', $numericStr == $one); + assertType('bool', $numericStr == $zero); + assertType('bool', $numericStr == $minusOne); + assertType('bool', $numericStr == $oneStr); + assertType('bool', $numericStr == $zeroStr); + assertType('bool', $numericStr == $minusOneStr); + assertType('bool', $numericStr == $plusOneStr); + assertType('false', $numericStr == $null); + assertType('bool', $numericStr == $stringOrNull); + assertType('false', $numericStr == $emptyArr); + assertType('bool', $numericStr == $string); + assertType('false', $numericStr == $phpStr); + assertType('false', $numericStr == $emptyStr); + if (is_numeric($string)) { + assertType('bool', $numericStr == $string); + } + + assertType('false', "" == 1); + assertType('true', "" == null); + assertType('false', "" == true); + assertType('true', "" == false); + assertType('false', "" == "1"); + assertType('false', "" == "0"); + assertType('false', "" == "-1"); + assertType('false', "" == []); } /** @@ -601,4 +697,267 @@ public function sayEmptyStr( assertType('false', $emptyStr == $phpStr); assertType('true', $emptyStr == $emptyStr); } + + /** + * @param true $true + * @param false $false + * @param 1 $one + * @param 0 $zero + * @param -1 $minusOne + * @param '1' $oneStr + * @param '0' $zeroStr + * @param '-1' $minusOneStr + * @param '+1' $plusOneStr + * @param null $null + * @param array{} $emptyArr + * @param 'php' $phpStr + * @param '' $emptyStr + * @param int<10, 20> $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 @@ + $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')); +}; 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..a4d4121e18 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mb-convert-encoding-php8.php @@ -0,0 +1,45 @@ += 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'])); +}; diff --git a/tests/PHPStan/Analyser/nsrt/memcache-get.php b/tests/PHPStan/Analyser/nsrt/memcache-get.php index 735e85b545..533e483682 100644 --- a/tests/PHPStan/Analyser/nsrt/memcache-get.php +++ b/tests/PHPStan/Analyser/nsrt/memcache-get.php @@ -10,5 +10,5 @@ function (): void { $memcache = new Memcache(); assertType('mixed', $memcache->get("key1")); - assertType('array|false', $memcache->get(array("key1", "key2", "key3"))); + assertType('array|false', $memcache->get(array("key1", "key2", "key3"))); }; 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 index 97cc9d0842..fee2d7bed4 100644 --- a/tests/PHPStan/Analyser/nsrt/mixed-to-number.php +++ b/tests/PHPStan/Analyser/nsrt/mixed-to-number.php @@ -35,7 +35,7 @@ function doFoo($mixed, int $i, float $f, $numericS) { } if (!is_array($mixed)) { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); assertType('(float|int)', $mixed + $mixed); } } diff --git a/tests/PHPStan/Analyser/nsrt/mixed-typehint.php b/tests/PHPStan/Analyser/nsrt/mixed-typehint.php index 8d7ce4ad16..5b3c17cbb1 100644 --- a/tests/PHPStan/Analyser/nsrt/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/more-type-strings-php8.php b/tests/PHPStan/Analyser/nsrt/more-type-strings-php8.php new file mode 100644 index 0000000000..d81c6e9907 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/more-type-strings-php8.php @@ -0,0 +1,48 @@ += 8.1 + +namespace MoreTypeStringsPhp8; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @param interface-string $interfaceString + * @param trait-string $traitString + * @param interface-string $genericInterfaceString + * @param trait-string $genericTraitString + * @param enum-string $genericEnumString + * @param enum-string $genericInterfaceEnumString + */ + public function doFoo( + string $interfaceString, + string $traitString, + string $genericInterfaceString, + string $genericTraitString, + string $genericEnumString, + string $genericInterfaceEnumString, + ): void + { + assertType('class-string', $interfaceString); + assertType('class-string', $traitString); + assertType('class-string', $genericInterfaceString); + assertType('string', $genericTraitString); + assertType('class-string', $genericEnumString); + assertType('class-string', $genericInterfaceEnumString); + } + +} + +enum Bar +{ + + case A; + case B; + +} + +interface BuzInterface +{ + +} diff --git a/tests/PHPStan/Analyser/nsrt/more-types.php b/tests/PHPStan/Analyser/nsrt/more-types.php index f1b742f170..c8ae927b2d 100644 --- a/tests/PHPStan/Analyser/nsrt/more-types.php +++ b/tests/PHPStan/Analyser/nsrt/more-types.php @@ -17,6 +17,10 @@ class Foo * @param non-empty-scalar $nonEmptyScalar * @param empty-scalar $emptyScalar * @param non-empty-mixed $nonEmptyMixed + * @param lowercase-string $lowercaseString + * @param non-empty-lowercase-string $nonEmptyLowercaseString + * @param uppercase-string $uppercaseString + * @param non-empty-uppercase-string $nonEmptyUppercaseString */ public function doFoo( $pureCallable, @@ -27,18 +31,26 @@ public function doFoo( $nonEmptyLiteralString, $nonEmptyScalar, $emptyScalar, - $nonEmptyMixed + $nonEmptyMixed, + $lowercaseString, + $nonEmptyLowercaseString, + $uppercaseString, + $nonEmptyUppercaseString, ): void { assertType('pure-callable(): mixed', $pureCallable); assertType('array&callable(): mixed', $callableArray); assertType('resource', $closedResource); assertType('resource', $openResource); - assertType('class-string', $enumString); + assertType('class-string', $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("mixed~(0|0.0|''|'0'|array{}|false|null)", $nonEmptyMixed); + assertType('lowercase-string', $lowercaseString); + assertType('lowercase-string&non-empty-string', $nonEmptyLowercaseString); + assertType('uppercase-string', $uppercaseString); + assertType('non-empty-string&uppercase-string', $nonEmptyUppercaseString); } } diff --git a/tests/PHPStan/Analyser/nsrt/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/native-expressions.php b/tests/PHPStan/Analyser/nsrt/native-expressions.php index 72f5a47e7e..abe041686a 100644 --- a/tests/PHPStan/Analyser/nsrt/native-expressions.php +++ b/tests/PHPStan/Analyser/nsrt/native-expressions.php @@ -34,7 +34,7 @@ public function __construct( /** @var non-empty-array */ private array $array ){ - assertType('non-empty-array', $this->array); + assertType('non-empty-array', $this->array); assertNativeType('array', $this->array); if(count($array) === 0){ throw new \InvalidArgumentException(); diff --git a/tests/PHPStan/Analyser/nsrt/native-types.php b/tests/PHPStan/Analyser/nsrt/native-types.php index 3f09d923cb..6c216d23a5 100644 --- a/tests/PHPStan/Analyser/nsrt/native-types.php +++ b/tests/PHPStan/Analyser/nsrt/native-types.php @@ -1,4 +1,4 @@ -= 7.4 +', $a); + assertType('array{4, 5, 6}', $a); assertNativeType('array', $a); } diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-array.php b/tests/PHPStan/Analyser/nsrt/non-empty-array.php index a7cdc6540a..d28aad3556 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-array.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-array.php @@ -26,7 +26,7 @@ public function doFoo( ): void { assertType('non-empty-array', $array); - assertType('non-empty-list', $list); + assertType('non-empty-list', $list); assertType('non-empty-array', $arrayOfStrings); assertType('non-empty-list', $listOfStd); assertType('non-empty-list', $listOfStd2); @@ -54,7 +54,7 @@ public function arrayFunctions($array, $list, $stringArray): void assertType('non-empty-array', array_replace($array, [])); assertType('non-empty-array', array_replace($array, $array)); - assertType('non-empty-array', array_flip($array)); + assertType('non-empty-array<(int|string)>', array_flip($array)); assertType('non-empty-array', array_flip($stringArray)); } } diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php index 3627b8a409..a774b4eba4 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php @@ -26,9 +26,9 @@ public function replace(string $search, string $replacement, string $subject){ function foo(float $f) { $s = (string) $f; - assertType('numeric-string', $s); + assertType('numeric-string&uppercase-string', $s); $price = str_replace(',', '.', $s); - assertType('non-empty-string', $price); + assertType('non-empty-string&uppercase-string', $price); } } diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php index 12d3495098..19482b7fd0 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php @@ -14,6 +14,16 @@ class Foo { */ public function strContains(string $s, string $s2, $nonES, $nonFalsy, $numS, $literalS, $nonEAndNumericS, int $i): void { + if (str_contains($i, 0)) { + assertType('int', $i); + } + if (str_contains($s, 0)) { + assertType('non-empty-string', $s); + } + if (str_contains($s, 1)) { + assertType('non-falsy-string', $s); + } + if (str_contains($s, ':')) { assertType('non-falsy-string', $s); } diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string-substr-specifying.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-substr-specifying.php index 6307af45e9..8ad670a7f8 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string-substr-specifying.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-substr-specifying.php @@ -4,7 +4,8 @@ use function PHPStan\Testing\assertType; -class Foo { +class Foo +{ public function nonEmptySubstr(string $s, int $offset, int $length): void { if (substr($s, 10) === 'hallo') { @@ -81,4 +82,19 @@ public function nonEmptySubstr(string $s, int $offset, int $length): void assertType('\'hallo\'', $x); } } + + /** + * @param non-empty-string $nonES + * @param non-falsy-string $falsyString + */ + public function stringTypes(string $s, $nonES, $falsyString): void + { + if (substr($s, 10) === $nonES) { + assertType('non-empty-string', $s); + } + + if (substr($s, 10) === $falsyString) { + assertType('non-falsy-string', $s); + } + } } diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string-substr.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-substr.php index c1f85c4df7..42dacb886f 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string-substr.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-substr.php @@ -54,7 +54,7 @@ public function doMbSubstr(string $s, $nonEmpty, $positiveInt, $postiveRange, $n assertType('string', mb_substr($s, 0, $positiveInt)); assertType('non-empty-string', mb_substr($nonEmpty, 0, $positiveInt)); - assertType('non-empty-string', mb_substr("déjà_vu", 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 index 976071cb84..c8031310ae 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -8,6 +8,7 @@ use function strtolower; use function strtoupper; use function ucfirst; +use const ENT_SUBSTITUTE; class Foo { @@ -127,7 +128,7 @@ public function doFoo3(string $s): void */ public function doFoo4(string $s): void { - assertType('non-empty-list', explode($s, 'foo')); + assertType('non-empty-list', explode($s, 'foo')); } /** @@ -212,7 +213,8 @@ public function sayHello(int $i): void // coming from issue #5291 $s = array(1, $i); - assertType('non-falsy-string', implode("a", $s)); + assertType('lowercase-string&non-falsy-string', implode("a", $s)); + assertType('non-falsy-string&uppercase-string', implode("A", $s)); } /** @@ -233,7 +235,7 @@ public function sayHello2(int $i): void // coming from issue #5291 $s = array(1, $i); - assertType('non-falsy-string', join("a", $s)); + assertType('lowercase-string&non-falsy-string', join("a", $s)); } /** @@ -318,14 +320,14 @@ public function doFoo(string $s, string $nonEmpty, string $nonFalsy, int $i, boo 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('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)); @@ -333,9 +335,17 @@ public function doFoo(string $s, string $nonEmpty, string $nonFalsy, int $i, boo 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)); @@ -364,6 +374,13 @@ public function doFoo(string $s, string $nonEmpty, string $nonFalsy, int $i, boo 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)); @@ -408,4 +425,40 @@ function multiplesPrintfFormats(string $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 index f8c6dc40b7..12ca4c3fac 100644 --- a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php @@ -3,6 +3,7 @@ namespace NonFalseyString; use function PHPStan\Testing\assertType; +use const ENT_SUBSTITUTE; class Foo { /** @@ -10,7 +11,7 @@ class Foo { * @param truthy-string $truthyString */ public function bar($nonFalseyString, $truthyString) { - assertType('int', (int) $nonFalseyString); + assertType('int', (int) $nonFalseyString); // Do not remove `0` since `(int) '00'` is still `0` // truthy-string is an alias for non-falsy-string assertType('non-falsy-string', $truthyString); } @@ -87,15 +88,20 @@ function stringFunctions(string $s, $nonFalsey, $arrayOfNonFalsey, $nonEmptyArra assertType('non-falsy-string', escapeshellarg($nonFalsey)); assertType('non-falsy-string', escapeshellcmd($nonFalsey)); - assertType('non-falsy-string', strtoupper($nonFalsey)); - assertType('non-falsy-string', strtolower($nonFalsey)); - assertType('non-falsy-string', mb_strtoupper($nonFalsey)); - assertType('non-falsy-string', mb_strtolower($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)); diff --git a/tests/PHPStan/Analyser/nsrt/nullable-closure-parameter.php b/tests/PHPStan/Analyser/nsrt/nullable-closure-parameter.php index 86eb0c1e87..0758feb78c 100644 --- a/tests/PHPStan/Analyser/nsrt/nullable-closure-parameter.php +++ b/tests/PHPStan/Analyser/nsrt/nullable-closure-parameter.php @@ -1,4 +1,4 @@ -= 7.4 += 8.0 namespace OffsetAccess; 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 @@ += 7.4 += 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/pow.php b/tests/PHPStan/Analyser/nsrt/pow.php index 328de3f172..3ca27690db 100644 --- a/tests/PHPStan/Analyser/nsrt/pow.php +++ b/tests/PHPStan/Analyser/nsrt/pow.php @@ -47,11 +47,11 @@ function (): void { $x = 4; } - assertType('int<4, 27>|int<16, 81>', pow($range, $x)); - assertType('int<4, 27>|int<16, 81>', $range ** $x); + assertType('int<4, 81>', pow($range, $x)); + assertType('int<4, 81>', $range ** $x); - assertType('int<4, 27>|int<16, 64>', pow($x, $range)); - assertType('int<4, 27>|int<16, 64>', $x ** $range); + 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); 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 index 6aec5018a0..a8c0fc19ae 100644 --- a/tests/PHPStan/Analyser/nsrt/pr-1244.php +++ b/tests/PHPStan/Analyser/nsrt/pr-1244.php @@ -1,4 +1,4 @@ -= 7.4 +', PHP_MAJOR_VERSION); +assertType('int<5, 8>', PHP_MAJOR_VERSION); assertType('int<0, max>', PHP_MINOR_VERSION); assertType('int<0, max>', PHP_RELEASE_VERSION); -assertType('int<50207, max>', PHP_VERSION_ID); +assertType('int<50207, 80599>', PHP_VERSION_ID); assertType('string', PHP_EXTRA_VERSION); assertType('0|1', PHP_ZTS); assertType('0|1', PHP_DEBUG); @@ -45,7 +45,7 @@ assertType('4096', E_RECOVERABLE_ERROR); assertType('8192', E_DEPRECATED); assertType('16384', E_USER_DEPRECATED); -assertType('32767', E_ALL); +assertType('int', E_ALL); assertType('2048', E_STRICT); assertType('int<1, max>', __COMPILER_HALT_OFFSET__); assertType('true', true); diff --git a/tests/PHPStan/Analyser/nsrt/preg_filter.php b/tests/PHPStan/Analyser/nsrt/preg_filter.php index 02824d3c9c..aedf0bca2a 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_filter.php +++ b/tests/PHPStan/Analyser/nsrt/preg_filter.php @@ -24,10 +24,10 @@ function doFoo1() { function doFoo2() { $subject = 123; - assertType('list|string|null', preg_filter('/\d/', '$0', $subject)); + assertType('string|null', preg_filter('/\d/', '$0', $subject)); $subject = 123.123; - assertType('list|string|null', preg_filter('/\d/', '$0', $subject)); + assertType('string|null', preg_filter('/\d/', '$0', $subject)); } public function dooFoo3(string $pattern, string $replace) { diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php index c415fca42f..7ed783a8e9 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php @@ -175,3 +175,12 @@ function doFoobarNull(string $s): void { } } } + +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_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 001ec26d21..545fd191f1 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -7,117 +7,117 @@ function doMatch(string $s): void { if (preg_match('/Price: /i', $s, $matches)) { - assertType('array{string}', $matches); + assertType('array{non-falsy-string}', $matches); } - assertType('array{}|array{string}', $matches); + assertType('array{}|array{non-falsy-string}', $matches); if (preg_match('/Price: (£|€)\d+/', $s, $matches)) { - assertType('array{string, non-empty-string}', $matches); + assertType("array{non-falsy-string, '£'|'€'}", $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{string, non-empty-string}', $matches); + assertType("array{}|array{non-falsy-string, '£'|'€'}", $matches); if (preg_match('/Price: (£|€)(\d+)/i', $s, $matches)) { - assertType('array{string, non-empty-string, numeric-string}', $matches); + assertType('array{non-falsy-string, non-empty-string, numeric-string}', $matches); } - assertType('array{}|array{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{string, non-empty-string}', $matches); + assertType('array{non-falsy-string, non-empty-string}', $matches); } - assertType('array{}|array{string, non-empty-string}', $matches); + assertType('array{}|array{non-falsy-string, non-empty-string}', $matches); if (preg_match('(Price: (£|€))i', $s, $matches)) { - assertType('array{string, non-empty-string}', $matches); + assertType('array{non-falsy-string, non-empty-string}', $matches); } - assertType('array{}|array{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{string, non-empty-string}', $matches); + assertType('array{non-falsy-string, non-empty-string}', $matches); } - assertType('array{}|array{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: string, 1: 'a', 2: string, 3: 'c', 4?: non-empty-string}", $matches); + assertType("array{0: non-falsy-string, 1: 'a', 2: string, 3: 'c', 4?: non-empty-string}", $matches); } - assertType("array{}|array{0: 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: string, 1: 'a', name: string, 2: string, 3: 'c', 4?: non-empty-string}", $matches); + assertType("array{0: non-falsy-string, 1: 'a', name: string, 2: string, 3: 'c', 4?: non-empty-string}", $matches); } - assertType("array{}|array{0: 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: string, 1: 'a', 2: string, 3: 'c', name?: non-empty-string, 4?: non-empty-string}", $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: 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: string, 1?: non-empty-string}', $matches); + assertType("array{0: non-empty-string, 1?: 'a'|'b'}", $matches); } - assertType('array{}|array{0: string, 1?: non-empty-string}', $matches); + assertType("array{}|array{0: non-empty-string, 1?: 'a'|'b'}", $matches); if (preg_match('/(foo)(bar)(baz)+/', $s, $matches)) { - assertType("array{string, 'foo', 'bar', non-falsy-string}", $matches); + assertType("array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); } - assertType("array{}|array{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: string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); + assertType("array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); } - assertType("array{}|array{0: 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: string, 1: 'foo', 2: 'bar', 3?: 'baz'}", $matches); + assertType("array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: 'baz'}", $matches); } - assertType("array{}|array{0: 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: string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); + assertType("array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); } - assertType("array{}|array{0: 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{string, 'foo', 'bar', non-falsy-string}", $matches); + assertType("array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); } - assertType("array{}|array{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{string, 'foo', 'bar', non-falsy-string}", $matches); + assertType("array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); } - assertType("array{}|array{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{string, numeric-string}', $matches); + assertType('array{non-falsy-string, numeric-string}', $matches); } - assertType('array{}|array{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: string, num: numeric-string, 1: numeric-string, 2: non-empty-string}', $matches); + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string, 2: non-empty-string}', $matches); } - assertType('array{}|array{0: 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: string, name: non-falsy-string, 1: non-falsy-string}', $matches); + assertType('array{0: non-falsy-string, name: non-falsy-string, 1: non-falsy-string}', $matches); } - assertType('array{}|array{0: 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: string, name: non-falsy-string, 1: non-falsy-string, dataname?: non-falsy-string, 2?: non-falsy-string}', $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: 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{string, int<-1, max>}, array{'foo', int<-1, max>}, array{'bar', int<-1, max>}, array{'baz', int<-1, max>}}", $matches); + 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{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 { @@ -129,91 +129,91 @@ function doUnknownFlags(string $s, int $flags): void { function doMultipleAlternativeCaptureGroupsWithSameNameWithModifier(string $s): void { if (preg_match('/(?J)(?[a-z]+)|(?[0-9]+)/', $s, $matches)) { - assertType("array{0: string, Foo: non-empty-string, 1: non-empty-string}|array{0: string, Foo: numeric-string, 1: '', 2: numeric-string}", $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: string, Foo: non-empty-string, 1: non-empty-string}|array{0: 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: string, Foo: non-empty-string, 1: non-empty-string}|array{0: string, Foo: numeric-string, 1: '', 2: numeric-string}", $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: string, Foo: non-empty-string, 1: non-empty-string}|array{0: 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{string, non-empty-string}', $matches); + assertType('array{non-empty-string, non-empty-string}', $matches); } - assertType('array{}|array{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{string, numeric-string, non-empty-string}', $matches); + assertType('array{non-falsy-string, numeric-string, non-empty-string}', $matches); } - assertType('array{}|array{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{string}', $matches); + assertType('array{non-falsy-string}', $matches); } - assertType('array{}|array{string}', $matches); + assertType('array{}|array{non-falsy-string}', $matches); } function testPregMatchSimpleCondition(string $value): void { if (preg_match('/%env\((.*)\:.*\)%/U', $value, $matches)) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } function testPregMatchIdenticalToOne(string $value): void { if (preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) === 1) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } function testPregMatchIdenticalToOneFalseyContext(string $value): void { if (!(preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) !== 1)) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } function testPregMatchIdenticalToOneInverted(string $value): void { if (1 === preg_match('/%env\((.*)\:.*\)%/U', $value, $matches)) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } function testPregMatchIdenticalToOneFalseyContextInverted(string $value): void { if (!(1 !== preg_match('/%env\((.*)\:.*\)%/U', $value, $matches))) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } function testPregMatchEqualToOne(string $value): void { if (preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) == 1) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } function testPregMatchEqualToOneFalseyContext(string $value): void { if (!(preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) != 1)) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } function testPregMatchEqualToOneInverted(string $value): void { if (1 == preg_match('/%env\((.*)\:.*\)%/U', $value, $matches)) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } function testPregMatchEqualToOneFalseyContextInverted(string $value): void { if (!(1 != preg_match('/%env\((.*)\:.*\)%/U', $value, $matches))) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } @@ -225,18 +225,18 @@ function testUnionPattern(string $s): void $pattern = '/Price: (\d+)(\d+)(\d+)/'; } if (preg_match($pattern, $s, $matches)) { - assertType('array{string, numeric-string, numeric-string, numeric-string}|array{string, numeric-string}', $matches); + assertType('array{non-falsy-string, numeric-string, numeric-string, numeric-string}|array{non-falsy-string, numeric-string}', $matches); } - assertType('array{}|array{string, numeric-string, numeric-string, numeric-string}|array{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{string, 'ab', 'b'}", $matches); + assertType("array{non-falsy-string, 'ab', 'b'}", $matches); } if (preg_match('~^(a(b)?)$~', $row, $matches) === 1) { - assertType("array{0: string, 1: non-falsy-string, 2?: 'b'}", $matches); + 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); @@ -249,7 +249,7 @@ function doFoo2(string $row): void return; } - assertType("array{0: 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); + 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 @@ -258,56 +258,56 @@ function doFoo3(string $row): void return; } - assertType('array{string, non-falsy-string, non-falsy-string, numeric-string, numeric-string, numeric-string, numeric-string}', $matches); + 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{string, non-falsy-string, numeric-string, numeric-string, non-empty-string}|array{string}', $matches); + 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{string, non-falsy-string, numeric-string}|array{string}', $matches); + 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: string, 1: non-falsy-string, 2?: numeric-string}', $matches); + 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: string, 1?: non-falsy-string, 2?: numeric-string}', $matches); + 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{string, non-falsy-string, numeric-string}', $matches); + 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("array{0: string, 1?: ''|'b', 2?: 'c'}", $matches); + 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{string, '', '', '', numeric-string}|array{string, '', '', numeric-string}|array{string, numeric-string, numeric-string}", $matches); + 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 { @@ -321,16 +321,16 @@ function (string $size): void { if (preg_match('~\{(?:(include)\\s+(?:[$]?\\w+(?£|€)\d+/', $s, $matches)) { - assertType('array{0: string, currency: non-empty-string, 1: non-empty-string}', $matches); + assertType("array{0: non-falsy-string, currency: '£'|'€', 1: '£'|'€'}", $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{0: string, currency: non-empty-string, 1: non-empty-string}', $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: string, currency: non-empty-string, 1: non-empty-string}', $matches); + assertType("array{0: non-falsy-string, currency: '£'|'€', 1: '£'|'€'}", $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{0: string, currency: non-empty-string, 1: non-empty-string}', $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: string, currency: non-empty-string, 1: non-empty-string}', $matches); + assertType("array{0: non-falsy-string, currency: '£'|'€', 1: '£'|'€'}", $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{0: string, currency: non-empty-string, 1: non-empty-string}', $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{string, 'z'}", $matches); + assertType("array{non-falsy-string, 'z'}", $matches); } else { assertType('array{}', $matches); } - assertType("array{}|array{string, 'z'}", $matches); + assertType("array{}|array{non-falsy-string, 'z'}", $matches); }; function (string $s): void { if (preg_match('{' . preg_quote($s) . '(z)}', $s, $matches)) { - assertType("array{string, 'z'}", $matches); + assertType("array{non-falsy-string, 'z'}", $matches); } else { assertType('array{}', $matches); } - assertType("array{}|array{string, 'z'}", $matches); + assertType("array{}|array{non-falsy-string, 'z'}", $matches); }; function (string $s): void { if (preg_match('/' . preg_quote($s, '/') . '(\d)/', $s, $matches)) { - assertType('array{string, numeric-string}', $matches); + assertType('array{non-empty-string, numeric-string}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{string, numeric-string}', $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: string, 1: 'z', 2?: 'def'}", $matches); + assertType("array{0: non-falsy-string, 1: 'z', 2?: 'def'}", $matches); } else { assertType('array{}', $matches); } - assertType("array{}|array{0: string, 1: 'z', 2?: 'def'}", $matches); + assertType("array{}|array{0: non-falsy-string, 1: 'z', 2?: 'def'}", $matches); }; function (string $s, $mixed): void { @@ -429,100 +429,103 @@ function (string $s, $mixed): void { function (string $s): void { if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $s, $matches) === 1) { - assertType("array{string, string, 'b'|'d'|'E'|'e'|'F'|'f'|'G'|'g'|'H'|'h'|'o'|'s'|'u'|'X'|'x'}", $matches); + 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{string, non-falsy-string, numeric-string}", $matches); + 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{string, non-falsy-string, numeric-string}", $matches); + assertType("array{non-falsy-string, non-falsy-string, numeric-string}", $matches); } }; function (string $s): void { if (preg_match('~^([157])$~', $s, $matches) === 1) { - assertType("array{string, '1'|'5'|'7'}", $matches); + assertType("array{non-falsy-string, '1'|'5'|'7'}", $matches); } }; function (string $s): void { if (preg_match('~^([157XY])$~', $s, $matches) === 1) { - assertType("array{string, '1'|'5'|'7'|'X'|'Y'}", $matches); + assertType("array{non-falsy-string, '1'|'5'|'7'|'X'|'Y'}", $matches); } }; function bug11323(string $s): void { if (preg_match('/([*|+?{}()]+)([^*|+[:digit:]?{}()]+)/', $s, $matches)) { - assertType('array{string, non-empty-string, non-empty-string}', $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{string, non-empty-string, non-empty-string}', $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{string, non-falsy-string, 'a-z'}", $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{string, numeric-string, non-falsy-string}', $matches); + assertType('array{non-falsy-string, numeric-string, non-falsy-string}', $matches); } if (preg_match('{([]] [^]])}', $s, $matches)) { - assertType('array{string, non-falsy-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string}', $matches); } if (preg_match('{([[:digit:]])}', $s, $matches)) { - assertType('array{string, numeric-string}', $matches); + assertType('array{non-empty-string, numeric-string}', $matches); } if (preg_match('{([\d])(\d)}', $s, $matches)) { - assertType('array{string, numeric-string, numeric-string}', $matches); + assertType('array{non-falsy-string, numeric-string, numeric-string}', $matches); } if (preg_match('{([0-9])}', $s, $matches)) { - assertType('array{string, numeric-string}', $matches); + assertType('array{non-empty-string, numeric-string}', $matches); } if (preg_match('{(\p{L})(\p{P})(\p{Po})}', $s, $matches)) { - assertType('array{string, non-empty-string, non-empty-string, non-empty-string}', $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{string, ''|'a', string, non-empty-string, non-empty-string}", $matches); + assertType("array{non-falsy-string, ''|'a', string, non-empty-string, non-empty-string}", $matches); } if (preg_match('{(.\d)}', $s, $matches)) { - assertType('array{string, non-falsy-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string}', $matches); } if (preg_match('{(\d.)}', $s, $matches)) { - assertType('array{string, non-falsy-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string}', $matches); } if (preg_match('{(\d\d)}', $s, $matches)) { - assertType('array{string, non-falsy-string&numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); } if (preg_match('{(.(\d))}', $s, $matches)) { - assertType('array{string, non-falsy-string, numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, numeric-string}', $matches); } if (preg_match('{((\d).)}', $s, $matches)) { - assertType('array{string, non-falsy-string, numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, numeric-string}', $matches); } if (preg_match('{(\d([1-4])\d)}', $s, $matches)) { - assertType('array{string, non-falsy-string&numeric-string, numeric-string}', $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{string, non-falsy-string, numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, numeric-string}', $matches); } if (preg_match('{([^1-4])}', $s, $matches)) { - assertType('array{string, non-empty-string}', $matches); + assertType('array{non-empty-string, non-empty-string}', $matches); } if (preg_match("{([\r\n]+)(\n)([\n])}", $s, $matches)) { - assertType('array{string, non-empty-string, "\n", "\n"}', $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: string, 1?: 'x', MARK?: 'first'|'second'}", $matches); + assertType("array{0: non-empty-string, 1?: 'x', MARK?: 'first'|'second'}", $matches); } } function (string $s): void { preg_match('/%a(\d*)/', $s, $matches); - assertType("array{0?: string, 1?: ''|numeric-string}", $matches); + assertType("list{0?: string, 1?: ''|numeric-string}", $matches); }; class Bug11376 @@ -530,13 +533,13 @@ class Bug11376 public function test(string $str): void { preg_match('~^(?:(\w+)::)?(\w+)$~', $str, $matches); - assertType('array{0?: string, 1?: string, 2?: non-empty-string}', $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{string, string, non-empty-string}', $matches); + assertType('array{non-empty-string, string, non-empty-string}', $matches); } } } @@ -549,7 +552,7 @@ function (string $s): void { } if (preg_match($p, $s, $matches)) { - assertType("array{string, '£', 'abc'}|array{string, numeric-string, 'b'}", $matches); + assertType("array{non-falsy-string, '£', 'abc'}|array{non-falsy-string, numeric-string, 'b'}", $matches); } }; @@ -561,67 +564,97 @@ function (string $s): void { } if (preg_match($p, $s, $matches)) { - assertType("array{0: string, 1: non-empty-string, 2?: ''|numeric-string, 3?: 'x'}", $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{string, non-empty-string}", $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{string, numeric-string}", $matches); + assertType("array{non-falsy-string, numeric-string}", $matches); } }; function (string $s): void { if (preg_match('/Price: ([xXa])/i', $s, $matches)) { - assertType("array{string, non-empty-string}", $matches); + assertType("array{non-falsy-string, non-empty-string}", $matches); } }; function (string $s): void { if (preg_match('/Price: ([xXa])/', $s, $matches)) { - assertType("array{string, 'a'|'X'|'x'}", $matches); + assertType("array{non-falsy-string, 'a'|'X'|'x'}", $matches); } }; function (string $s): void { if (preg_match('/Price: (ba[rz])/', $s, $matches)) { - assertType("array{string, 'bar'|'baz'}", $matches); + assertType("array{non-falsy-string, 'bar'|'baz'}", $matches); } }; function (string $s): void { if (preg_match('/Price: (b[ao][mn])/', $s, $matches)) { - assertType("array{string, 'bam'|'ban'|'bom'|'bon'}", $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{string, non-empty-string}", $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{string, non-empty-string}", $matches); + assertType("array{non-falsy-string, '0'|'a'}", $matches); } }; function (string $s): void { if (preg_match('/Price: (aa|0)/', $s, $matches)) { - assertType("array{string, non-empty-string}", $matches); + assertType("array{non-falsy-string, '0'|'aa'}", $matches); } }; function (string $s): void { if (preg_match('/( \d+ )/x', $s, $matches)) { - assertType('array{string, numeric-string}', $matches); + assertType('array{non-empty-string, numeric-string}', $matches); } }; @@ -639,7 +672,7 @@ function (string $s): void { function (string $s): void { if (preg_match('/( .+ )/x', $s, $matches)) { - assertType('array{string, non-empty-string}', $matches); + assertType('array{non-empty-string, non-empty-string}', $matches); } }; @@ -679,32 +712,32 @@ static public function sayHello(string $source): void function (string $s): void { if (preg_match('~a|(\d)|(\s)~', $s, $matches) === 1) { - assertType("array{0: string, 1?: numeric-string}|array{string, '', non-empty-string}", $matches); + 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{string, '', '', 'vy', 'v'}|array{string, 'ux', 'u'}|array{string}", $matches); + 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{string, int<-1, max>}, 1?: array{numeric-string, int<-1, max>}}|array{array{string, int<-1, max>}, array{'', int<-1, max>}, array{non-empty-string, int<-1, max>}}", $matches); + 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("array{0?: string, 1?: '', 2?: non-empty-string}|array{0?: string, 1?: numeric-string}", $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{string, ''|'+'|'-', numeric-string}", $matches); + assertType("array{non-falsy-string, ''|'+'|'-', numeric-string}", $matches); } } @@ -712,7 +745,331 @@ function bug11490b (string $expression): void { $matches = []; if (preg_match('/([\\[+])?([\d]+)%/', $expression, $matches) === 1) { - assertType("array{string, ''|'+'|'[', numeric-string}", $matches); + 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 index 34b1b72756..4620565210 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php @@ -7,15 +7,15 @@ 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{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{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{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{string, numeric-string}', $matches); + assertType('array{non-empty-string, numeric-string}', $matches); } - assertType('array{}|array{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 index dfbcab477e..1b5dc597b7 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php @@ -8,39 +8,39 @@ // 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{string}', $matches); + assertType('array{non-empty-string}', $matches); } - assertType('array{}|array{string}', $matches); + assertType('array{}|array{non-empty-string}', $matches); if (preg_match('/(\d+)(?P\d+)/n', $s, $matches)) { - assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); } - assertType('array{}|array{0: 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: string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); } - assertType('array{}|array{0: 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: string, num: numeric-string, 1: numeric-string}', $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: string, num: numeric-string, 1: numeric-string}', $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: string, num: numeric-string, 1: numeric-string}', $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: string, num: numeric-string, 1: numeric-string}', $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 index 2a3f437a81..d5e650e708 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes-php72.php +++ b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes-php72.php @@ -8,7 +8,7 @@ function (string $s): void { preg_replace_callback( $s, function ($matches) { - assertType('array', $matches); + assertType('array', $matches); return ''; }, $s @@ -19,7 +19,7 @@ function (string $s): void { preg_replace_callback( '|

(\s*)\w|', function ($matches) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); return ''; }, $s diff --git a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php index 36b6a51f1f..7bd70492ee 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php @@ -1,4 +1,4 @@ -= 7.4 +}, 1?: array{''|'foo', int<-1, max>}, 2?: array{''|'bar', int<-1, max>}, 3?: array{'baz', int<-1, max>}}", $matches); + assertType("list{0: array{string, int<-1, max>}, 1?: array{''|'foo', int<-1, max>}, 2?: array{''|'bar', int<-1, max>}, 3?: array{'baz', int<-1, max>}}", $matches); return ''; }, $s, @@ -45,3 +45,14 @@ function ($matches) { 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 index 7122c16150..210a467372 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_split.php +++ b/tests/PHPStan/Analyser/nsrt/preg_split.php @@ -6,12 +6,77 @@ class HelloWorld { + private const NUMERIC_STRING_1 = "1"; + private const NUMERIC_STRING_NEGATIVE_1 = "-1"; + public function doFoo() { - assertType('list|false', preg_split('/-/', '1-2-3')); - assertType('list|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY)); - assertType('list}>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE)); - assertType('list}>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{''}|false", preg_split('/-/', '')); + assertType("array{}|false", preg_split('/-/', '', -1, PREG_SPLIT_NO_EMPTY)); + assertType("array{'1', '-', '2', '-', '3'}|false", preg_split('/ *(-) */', '1- 2-3', -1, PREG_SPLIT_DELIM_CAPTURE)); + assertType("array{array{'', 0}}|false", preg_split('/-/', '', -1, PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{}|false", preg_split('/-/', '', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{'1', '2', '3'}|false", preg_split('/-/', '1-2-3')); + assertType("array{'1', '2', '3'}|false", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY)); + assertType("array{'1', '3'}|false", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY)); + assertType("array{array{'1', 0}, array{'2', 2}, array{'3', 4}}|false", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{array{'1', 0}, array{'2', 2}, array{'3', 4}}|false", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{array{'1', 0}, array{'', 2}, array{'3', 3}}|false", preg_split('/-/', '1--3', -1, PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{array{'1', 0}, array{'3', 3}}|false", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + + assertType("array{'1', '2', '3'}|false", preg_split('/-/', '1-2-3', self::NUMERIC_STRING_NEGATIVE_1)); + assertType("array{'1', '2', '3'}|false", preg_split('/-/', '1-2-3', -1, self::NUMERIC_STRING_1)); + } + + public function doWithError() { + assertType('*ERROR*', preg_split('/[0-9a]', '1-2-3')); + assertType('*ERROR*', preg_split('/-/', '1-2-3', 'hogehoge')); + assertType('*ERROR*', preg_split('/-/', '1-2-3', -1, 'hogehoge')); + assertType('*ERROR*', preg_split('/-/', '1-2-3', [], self::NUMERIC_STRING_NEGATIVE_1)); + assertType('*ERROR*', preg_split('/-/', '1-2-3', null, self::NUMERIC_STRING_NEGATIVE_1)); + assertType('*ERROR*', preg_split('/-/', '1-2-3', -1, [])); + assertType('*ERROR*', preg_split('/-/', '1-2-3', -1, null)); + } + + public function doWithVariables(string $pattern, string $subject, int $offset, int $flags): void + { + assertType("array{'1', '2', '3'}|array{'1-2-3'}|false", preg_split('/-/', '1-2-3', $this->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)); } /** @@ -26,8 +91,7 @@ public static function splitWithOffset($pattern, $subject, $limit = -1, $flags = { 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)); + assertType('list}>|false', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags | PREG_SPLIT_NO_EMPTY)); } /** @@ -35,7 +99,8 @@ public static function splitWithOffset($pattern, $subject, $limit = -1, $flags = * @param string $subject * @param int $limit */ - public static function dynamicFlags($pattern, $subject, $limit = -1) { + public static function dynamicFlags($pattern, $subject, $limit = -1) + { $flags = PREG_SPLIT_OFFSET_CAPTURE; if ($subject === '1-2-3') { diff --git a/tests/PHPStan/Analyser/nsrt/prestashop-breakdowns-empty-array.php b/tests/PHPStan/Analyser/nsrt/prestashop-breakdowns-empty-array.php index b9e0920141..a115a32359 100644 --- a/tests/PHPStan/Analyser/nsrt/prestashop-breakdowns-empty-array.php +++ b/tests/PHPStan/Analyser/nsrt/prestashop-breakdowns-empty-array.php @@ -21,13 +21,13 @@ public function getTaxBreakdown($mixed, $arrayMixed): void foreach ($breakdowns as $type => $bd) { if (empty($bd)) { - 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); 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); } } - 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 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/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 index a24a91ced6..d390747fe6 100644 --- a/tests/PHPStan/Analyser/nsrt/reflection-type.php +++ b/tests/PHPStan/Analyser/nsrt/reflection-type.php @@ -1,4 +1,4 @@ -= 7.4 +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); + 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('array>', $methodAll); - assertType('array>', $methodAbc); + assertType('list>', $methodAll); + assertType('list>', $methodAbc); $paramAll = $reflectionParameter->getAttributes(); $paramAbc = $reflectionParameter->getAttributes(Abc::class); - assertType('array>', $paramAll); - assertType('array>', $paramAbc); + assertType('list>', $paramAll); + assertType('list>', $paramAbc); $propAll = $reflectionProperty->getAttributes(); $propAbc = $reflectionProperty->getAttributes(Abc::class); - assertType('array>', $propAll); - assertType('array>', $propAbc); + assertType('list>', $propAll); + assertType('list>', $propAbc); $constAll = $reflectionClassConstant->getAttributes(); $constAbc = $reflectionClassConstant->getAttributes(Abc::class); - assertType('array>', $constAll); - assertType('array>', $constAbc); + assertType('list>', $constAll); + assertType('list>', $constAbc); $funcAll = $reflectionFunction->getAttributes(); $funcAbc = $reflectionFunction->getAttributes(Abc::class); - assertType('array>', $funcAll); - assertType('array>', $funcAbc); + assertType('list>', $funcAll); + assertType('list>', $funcAbc); } /** 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-readonly-constructor-narrowed-hooks.php b/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed-hooks.php new file mode 100644 index 0000000000..8f03858767 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed-hooks.php @@ -0,0 +1,35 @@ += 8.4 + +namespace RememberReadOnlyConstructorInPropertyHookBodies; + +use function PHPStan\Testing\assertType; + +class User +{ + public string $name { + get { + assertType('1|2', $this->type); + return $this->name ; + } + set { + if (strlen($value) === 0) { + throw new ValueError("Name must be non-empty"); + } + assertType('1|2', $this->type); + $this->name = $value; + } + } + + private readonly int $type; + + public function __construct( + string $name + ) { + $this->name = $name; + if (rand(0,1)) { + $this->type = 1; + } else { + $this->type = 2; + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed.php b/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed.php new file mode 100644 index 0000000000..7ab2ea364f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed.php @@ -0,0 +1,109 @@ += 8.2 + +namespace RememberReadOnlyConstructor; + +use function PHPStan\Testing\assertType; + +class HelloWorldReadonlyProperty { + private readonly int $i; + + public function __construct() + { + if (rand(0,1)) { + $this->i = 4; + } else { + $this->i = 10; + } + } + + public function doFoo() { + assertType('4|10', $this->i); + } +} + +readonly class HelloWorldReadonlyClass { + private int $i; + private string $class; + private string $interface; + private string $enum; + private string $trait; + + public function __construct(string $class, string $interface, string $enum, string $trait) + { + if (rand(0,1)) { + $this->i = 4; + } else { + $this->i = 10; + } + + if (!class_exists($class)) { + throw new \LogicException(); + } + $this->class = $class; + + if (!interface_exists($interface)) { + throw new \LogicException(); + } + $this->interface = $interface; + + if (!enum_exists($enum)) { + throw new \LogicException(); + } + $this->enum = $enum; + + if (!trait_exists($trait)) { + throw new \LogicException(); + } + $this->trait = $trait; + } + + public function doFoo() { + assertType('4|10', $this->i); + assertType('class-string', $this->class); + assertType('class-string', $this->interface); + assertType('class-string', $this->enum); + assertType('class-string', $this->trait); + } +} + + +class HelloWorldRegular { + private int $i; + + public function __construct() + { + if (rand(0,1)) { + $this->i = 4; + } else { + $this->i = 10; + } + } + + public function doFoo() { + assertType('int', $this->i); + } +} + +class HelloWorldReadonlyPropertySometimesThrowing { + private readonly int $i; + + public function __construct() + { + if (rand(0,1)) { + $this->i = 4; + + return; + } elseif (rand(10,100)) { + $this->i = 10; + return; + } else { + $this->i = 20; + } + + throw new \LogicException(); + } + + public function doFoo() { + assertType('4|10', $this->i); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/self-out.php b/tests/PHPStan/Analyser/nsrt/self-out.php index d4de8dbf84..fa623d2d5a 100644 --- a/tests/PHPStan/Analyser/nsrt/self-out.php +++ b/tests/PHPStan/Analyser/nsrt/self-out.php @@ -50,6 +50,14 @@ public function setData($data) { */ public function test(): void { } + + /** + * @phpstan-self-out self + */ + public static function selfOutWithStaticMethod(): void + { + + } } /** @@ -94,4 +102,7 @@ function () { $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 index 11869c0623..f7513c6045 100644 --- a/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php +++ b/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php @@ -11,10 +11,10 @@ function doString(string $s, int $i, float $f, array $a, object $o) assertType('string', $s); settype($i, 'string'); - assertType('numeric-string', $i); + assertType('lowercase-string&numeric-string&uppercase-string', $i); settype($f, 'string'); - assertType('numeric-string', $f); + assertType('numeric-string&uppercase-string', $f); settype($a, 'string'); assertType('*ERROR*', $a); diff --git a/tests/PHPStan/Analyser/nsrt/shuffle.php b/tests/PHPStan/Analyser/nsrt/shuffle.php index c2bf853776..6b699e598a 100644 --- a/tests/PHPStan/Analyser/nsrt/shuffle.php +++ b/tests/PHPStan/Analyser/nsrt/shuffle.php @@ -13,7 +13,7 @@ public function normalArrays1(array $arr): void /** @var mixed[] $arr */ shuffle($arr); assertType('list', $arr); - assertNativeType('list', $arr); + assertNativeType('list', $arr); assertType('list>', array_keys($arr)); assertType('list', array_values($arr)); } @@ -23,7 +23,7 @@ public function normalArrays2(array $arr): void /** @var non-empty-array $arr */ shuffle($arr); assertType('non-empty-list', $arr); - assertNativeType('list', $arr); + assertNativeType('list', $arr); assertType('non-empty-list>', array_keys($arr)); assertType('non-empty-list', array_values($arr)); } @@ -33,10 +33,10 @@ 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', $arr); + assertNativeType('non-empty-list', $arr); assertType('non-empty-list>', array_keys($arr)); - assertType('non-empty-list', array_values($arr)); + assertType('non-empty-list', array_values($arr)); } } @@ -45,10 +45,10 @@ 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', $arr); + assertNativeType('non-empty-list', $arr); assertType('non-empty-list>', array_keys($arr)); - assertType('non-empty-list', array_values($arr)); + assertType('non-empty-list', array_values($arr)); } } @@ -66,8 +66,8 @@ public function constantArrays2(array $arr): void { /** @var array{0?: 1, 1?: 2, 2?: 3} $arr */ shuffle($arr); - assertType('array<0|1|2, 1|2|3>&list', $arr); - assertNativeType('list', $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)); } @@ -76,8 +76,8 @@ public function constantArrays3(array $arr): void { $arr = [1, 2, 3]; shuffle($arr); - assertType('non-empty-array<0|1|2, 1|2|3>&list', $arr); - assertNativeType('non-empty-array<0|1|2, 1|2|3>&list', $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)); } @@ -86,8 +86,8 @@ public function constantArrays4(array $arr): void { $arr = ['a' => 1, 'b' => 2, 'c' => 3]; shuffle($arr); - assertType('non-empty-array<0|1|2, 1|2|3>&list', $arr); - assertNativeType('non-empty-array<0|1|2, 1|2|3>&list', $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)); } @@ -96,8 +96,8 @@ public function constantArrays5(array $arr): void { $arr = [0 => 1, 3 => 2, 42 => 3]; shuffle($arr); - assertType('non-empty-array<0|1|2, 1|2|3>&list', $arr); - assertNativeType('non-empty-array<0|1|2, 1|2|3>&list', $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)); } @@ -106,8 +106,8 @@ public function constantArrays6(array $arr): void { /** @var array{foo?: 1, bar: 2, }|array{baz: 3, foobar?: 4} $arr */ shuffle($arr); - assertType('non-empty-array<0|1, 1|2|3|4>&list', $arr); - assertNativeType('list', $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)); } @@ -115,10 +115,10 @@ public function constantArrays6(array $arr): void public function mixed($arr): void { shuffle($arr); - assertType('list', $arr); - assertNativeType('list', $arr); + assertType('list', $arr); + assertNativeType('list', $arr); assertType('list>', array_keys($arr)); - assertType('list', array_values($arr)); + assertType('list', array_values($arr)); } public function subtractedArray($arr): void @@ -134,7 +134,7 @@ public function subtractedArray($arr): void assertType('*ERROR*', $arr); assertNativeType('*ERROR*', $arr); assertType('list', array_keys($arr)); - assertType('list', array_values($arr)); + assertType('list', array_values($arr)); } } 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 index e92b006713..93dfe0d147 100644 --- a/tests/PHPStan/Analyser/nsrt/sort.php +++ b/tests/PHPStan/Analyser/nsrt/sort.php @@ -91,17 +91,17 @@ public function normalArray(array $arr): void $arr1 = $arr; sort($arr1); assertType('list', $arr1); - assertNativeType('list', $arr1); + assertNativeType('list', $arr1); $arr2 = $arr; rsort($arr2); assertType('list', $arr2); - assertNativeType('list', $arr2); + assertNativeType('list', $arr2); $arr3 = $arr; usort($arr3, fn(int $a, int $b) => $a <=> $b); assertType('list', $arr3); - assertNativeType('list', $arr3); + assertNativeType('list', $arr3); } public function mixed($arr): void diff --git a/tests/PHPStan/Analyser/nsrt/static-late-binding.php b/tests/PHPStan/Analyser/nsrt/static-late-binding.php new file mode 100644 index 0000000000..53313ceddd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/static-late-binding.php @@ -0,0 +1,151 @@ +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/nsrt/str-casing.php b/tests/PHPStan/Analyser/nsrt/str-casing.php index df4883845a..ebdbd8054d 100644 --- a/tests/PHPStan/Analyser/nsrt/str-casing.php +++ b/tests/PHPStan/Analyser/nsrt/str-casing.php @@ -8,13 +8,14 @@ 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, $literal, $edgeUnion, $caseMode, $kanaMode, $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')); @@ -37,51 +38,65 @@ public function bar($numericS, $nonE, $literal, $edgeUnion, $caseMode, $kanaMode assertType("'Abc123アガば漢'|'Abc123あか゛ば漢'|'Abc123アカ゛ば漢'|'Abc123アガば漢'|'Abc123アガバ漢'", mb_convert_kana('Abc123アガば漢', $kanaMode)); assertType("non-falsy-string", mb_convert_kana('Abc123アガば漢', $mixed)); - assertType("numeric-string", strtolower($numericS)); - assertType("numeric-string", strtoupper($numericS)); - assertType("numeric-string", mb_strtolower($numericS)); - assertType("numeric-string", mb_strtoupper($numericS)); + 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", mb_convert_case($numericS, MB_CASE_UPPER)); - assertType("numeric-string", mb_convert_case($numericS, MB_CASE_LOWER)); + 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("non-empty-string", strtolower($nonE)); - assertType("non-empty-string", strtoupper($nonE)); - assertType("non-empty-string", mb_strtolower($nonE)); - assertType("non-empty-string", mb_strtoupper($nonE)); + 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", mb_convert_case($nonE, MB_CASE_UPPER)); - assertType("non-empty-string", mb_convert_case($nonE, MB_CASE_LOWER)); + 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("string", strtolower($literal)); - assertType("string", strtoupper($literal)); - assertType("string", mb_strtolower($literal)); - assertType("string", mb_strtoupper($literal)); + 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("string", mb_convert_case($literal, MB_CASE_UPPER)); - assertType("string", mb_convert_case($literal, MB_CASE_LOWER)); + 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("non-falsy-string", mb_strtolower("\xfe\xff\x65\xe5\x67\x2c\x8a\x9e", 'CP1252')); + 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 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 @@ + $twoOrThree * @param int<2, max> $twoOrMore * @param int $maxThree - * @param int<10, 11> $tenOrEleven + * @param 10|11 $tenOrEleven + * @param 0|11 $zeroOrEleven + * @param int<-10,-5> $negative */ -function doFoo(string $s, $zeroToThree, $twoOrThree, $twoOrMore, int $maxThree, $tenOrEleven): void +function doFoo(string $s, $zeroToThree, $twoOrThree, $twoOrMore, int $maxThree, $tenOrEleven, $zeroOrEleven, int $negative): void { if (strlen($s) >= $zeroToThree) { assertType('string', $s); @@ -51,4 +53,85 @@ function doFoo(string $s, $zeroToThree, $twoOrThree, $twoOrMore, int $maxThree, 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 @@ +', strval($class)); assertType('string', strval(new \Exception())); assertType('*ERROR*', strval(new \stdClass())); diff --git a/tests/PHPStan/Analyser/nsrt/superglobals.php b/tests/PHPStan/Analyser/nsrt/superglobals.php new file mode 100644 index 0000000000..ee7aadb686 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/superglobals.php @@ -0,0 +1,69 @@ +', $GLOBALS); + assertType('array', $_SERVER); + assertType('array', $_GET); + assertType('array', $_POST); + assertType('array', $_FILES); + assertType('array', $_COOKIE); + assertType('array', $_SESSION); + assertType('array', $_REQUEST); + assertType('array', $_ENV); + } + + public function canBeOverwritten(): void + { + $GLOBALS = []; + assertType('array{}', $GLOBALS); + assertNativeType('array{}', $GLOBALS); + } + + public function canBePartlyOverwritten(): void + { + $GLOBALS['foo'] = 'foo'; + assertType("non-empty-array&hasOffsetValue('foo', 'foo')", $GLOBALS); + assertNativeType("non-empty-array&hasOffsetValue('foo', 'foo')", $GLOBALS); + } + + public function canBeNarrowed(): void + { + if (isset($GLOBALS['foo'])) { + assertType("non-empty-array&hasOffsetValue('foo', mixed~null)", $GLOBALS); + assertNativeType("non-empty-array&hasOffset('foo')", $GLOBALS); // https://github.com/phpstan/phpstan/issues/8395 + } else { + assertType('array', $GLOBALS); + assertNativeType('array', $GLOBALS); + } + assertType('array', $GLOBALS); + assertNativeType('array', $GLOBALS); + } + +} + +function functionScope() { + assertType('array', $GLOBALS); + assertNativeType('array', $GLOBALS); +} + +assertType('array', $GLOBALS); +assertNativeType('array', $GLOBALS); + +function badNarrowing() { + if (empty($_GET['id'])) { + echo "b"; + } else { + echo "b"; + } + assertType('array', $_GET); + assertType('mixed', $_GET['id']); +}; diff --git a/tests/PHPStan/Analyser/nsrt/template-default.php b/tests/PHPStan/Analyser/nsrt/template-default.php new file mode 100644 index 0000000000..c73ce4ab38 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/template-default.php @@ -0,0 +1,136 @@ += 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/this-subtractable.php b/tests/PHPStan/Analyser/nsrt/this-subtractable.php index a087e3468b..a3d9a57554 100644 --- a/tests/PHPStan/Analyser/nsrt/this-subtractable.php +++ b/tests/PHPStan/Analyser/nsrt/this-subtractable.php @@ -12,7 +12,7 @@ public function doFoo() assertType('$this(ThisSubtractable\Foo)', $this); if (!$this instanceof Bar && !$this instanceof Baz) { - assertType('$this(ThisSubtractable\Foo~ThisSubtractable\Bar|ThisSubtractable\Baz)', $this); + assertType('$this(ThisSubtractable\Foo~(ThisSubtractable\Bar|ThisSubtractable\Baz))', $this); } else { assertType('($this(ThisSubtractable\Foo)&ThisSubtractable\Bar)|($this(ThisSubtractable\Foo)&ThisSubtractable\Baz)', $this); } @@ -26,7 +26,7 @@ public function doBar() assertType('static(ThisSubtractable\Foo)', $s); if (!$s instanceof Bar && !$s instanceof Baz) { - assertType('static(ThisSubtractable\Foo~ThisSubtractable\Bar|ThisSubtractable\Baz)', $s); + assertType('static(ThisSubtractable\Foo~(ThisSubtractable\Bar|ThisSubtractable\Baz))', $s); } else { assertType('(static(ThisSubtractable\Foo)&ThisSubtractable\Bar)|(static(ThisSubtractable\Foo)&ThisSubtractable\Baz)', $s); } @@ -52,7 +52,7 @@ public function doBazz(self $s) assertType('ThisSubtractable\Foo', $s); if (!$s instanceof Bar && !$s instanceof Baz) { - assertType('ThisSubtractable\Foo~ThisSubtractable\Bar|ThisSubtractable\Baz', $s); + assertType('ThisSubtractable\Foo~(ThisSubtractable\Bar|ThisSubtractable\Baz)', $s); } else { assertType('ThisSubtractable\Bar|ThisSubtractable\Baz', $s); } @@ -70,12 +70,12 @@ public function doBazzz(self $s) assertType('ThisSubtractable\Foo&hasMethod(test123)', $s); if (!$s instanceof Bar && !$s instanceof Baz) { - assertType('ThisSubtractable\Foo~ThisSubtractable\Bar|ThisSubtractable\Baz&hasMethod(test123)', $s); + 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); + assertType('(ThisSubtractable\Bar&hasMethod(test123))|(ThisSubtractable\Baz&hasMethod(test123))|(ThisSubtractable\Foo~(ThisSubtractable\Bar|ThisSubtractable\Baz)&hasMethod(test123))', $s); } /** diff --git a/tests/PHPStan/Analyser/nsrt/throw-points/try-catch.php b/tests/PHPStan/Analyser/nsrt/throw-points/try-catch.php index 57faddd9bb..87c0be66eb 100644 --- a/tests/PHPStan/Analyser/nsrt/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/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/nsrt/unset-conditional-expressions.php b/tests/PHPStan/Analyser/nsrt/unset-conditional-expressions.php index b310ffe1a5..afda5d2229 100644 --- a/tests/PHPStan/Analyser/nsrt/unset-conditional-expressions.php +++ b/tests/PHPStan/Analyser/nsrt/unset-conditional-expressions.php @@ -42,7 +42,7 @@ public function doBaz(): void } } - assertType('array{a?: bool, b?: numeric-string, c?: int<-1, 1>, d?: int<0, 1>}', $breakdowns); + 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 @@ +' $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/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/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 @@ +createMock(InputInterface::class), $output)), ); - $memoryLimitFile = self::getContainer()->getParameter('memoryLimitFile'); - $relativePathHelper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), __DIR__, [], DIRECTORY_SEPARATOR); $errorFormatter = new TableErrorFormatter( $relativePathHelper, @@ -88,20 +84,15 @@ private function runPath(string $path, int $expectedStatusCode): string true, null, null, + null, + null, $this->createMock(InputInterface::class), ); - if (file_exists($memoryLimitFile)) { - unlink($memoryLimitFile); - } $statusCode = $errorFormatter->formatErrors($analysisResult, $symfonyOutput); rewind($output->getStream()); $contents = stream_get_contents($output->getStream()); - if ($contents === false) { - throw new ShouldNotHappenException(); - } - $this->assertSame($expectedStatusCode, $statusCode, $contents); return $contents; diff --git a/tests/PHPStan/Command/AnalyseCommandTest.php b/tests/PHPStan/Command/AnalyseCommandTest.php index 2bec6dfa21..f2c4f4d73a 100644 --- a/tests/PHPStan/Command/AnalyseCommandTest.php +++ b/tests/PHPStan/Command/AnalyseCommandTest.php @@ -4,6 +4,8 @@ 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; @@ -14,15 +16,11 @@ use const DIRECTORY_SEPARATOR; use const PHP_EOL; -/** - * @group exec - */ +#[Group('exec')] class AnalyseCommandTest extends PHPStanTestCase { - /** - * @dataProvider autoDiscoveryPathsProvider - */ + #[DataProvider('autoDiscoveryPathsProvider')] public function testConfigurationAutoDiscovery(string $dir, string $file): void { $originalDir = getcwd(); @@ -78,16 +76,28 @@ public static function autoDiscoveryPathsProvider(): array { return [ [ - __DIR__ . '/test-autodiscover', - __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover' . DIRECTORY_SEPARATOR . 'phpstan.neon', + __DIR__ . '/test-autodiscover-dot', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-dot' . DIRECTORY_SEPARATOR . '.phpstan.neon', + ], + [ + __DIR__ . '/test-autodiscover-dot-dist', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-dot-dist' . DIRECTORY_SEPARATOR . '.phpstan.neon.dist', + ], + [ + __DIR__ . '/test-autodiscover-dot-dist-dot-neon', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-dot-dist-dot-neon' . DIRECTORY_SEPARATOR . '.phpstan.dist.neon', + ], + [ + __DIR__ . '/test-autodiscover-no-dot', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-no-dot' . DIRECTORY_SEPARATOR . 'phpstan.neon', ], [ - __DIR__ . '/test-autodiscover-dist', - __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-dist' . DIRECTORY_SEPARATOR . 'phpstan.neon.dist', + __DIR__ . '/test-autodiscover-no-dot-dist', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-no-dot-dist' . DIRECTORY_SEPARATOR . 'phpstan.neon.dist', ], [ - __DIR__ . '/test-autodiscover-dist-dot-neon', - __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-dist-dot-neon' . DIRECTORY_SEPARATOR . 'phpstan.dist.neon', + __DIR__ . '/test-autodiscover-no-dot-dist-dot-neon', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-no-dot-dist-dot-neon' . DIRECTORY_SEPARATOR . 'phpstan.dist.neon', ], [ __DIR__ . '/test-autodiscover-priority', @@ -97,6 +107,10 @@ public static function autoDiscoveryPathsProvider(): array __DIR__ . '/test-autodiscover-priority-dist-dot-neon', __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-priority-dist-dot-neon' . DIRECTORY_SEPARATOR . 'phpstan.neon', ], + [ + __DIR__ . '/test-autodiscover-priority-dot', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-priority-dot' . DIRECTORY_SEPARATOR . '.phpstan.neon', + ], ]; } diff --git a/tests/PHPStan/Command/CommandHelperTest.php b/tests/PHPStan/Command/CommandHelperTest.php index bbb00f122c..5ecf380ddb 100644 --- a/tests/PHPStan/Command/CommandHelperTest.php +++ b/tests/PHPStan/Command/CommandHelperTest.php @@ -3,6 +3,8 @@ 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; @@ -13,13 +15,11 @@ 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 [ [ @@ -94,9 +94,9 @@ public function dataBegin(): array } /** - * @dataProvider dataBegin * @param mixed[] $expectedParameters */ + #[DataProvider('dataBegin')] public function testBegin( string $input, string $expectedOutput, @@ -124,6 +124,10 @@ public function testBegin( null, $level, false, + false, + null, + null, + false, ); if ($expectException) { $this->fail(); @@ -132,9 +136,6 @@ public function testBegin( if (!$expectException) { rewind($output->getStream()); $contents = stream_get_contents($output->getStream()); - if ($contents === false) { - throw new ShouldNotHappenException(); - } $this->fail($contents); } } @@ -142,9 +143,6 @@ public function testBegin( rewind($output->getStream()); $contents = stream_get_contents($output->getStream()); - if ($contents === false) { - throw new ShouldNotHappenException(); - } $this->assertStringContainsString($expectedOutput, $contents); if (isset($result)) { @@ -158,7 +156,7 @@ public function testBegin( } } - public function dataParameters(): array + public static function dataParameters(): array { return [ [ @@ -167,7 +165,7 @@ 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', ], @@ -184,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', @@ -203,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#', @@ -287,10 +285,10 @@ public function dataParameters(): array } /** - * @dataProvider dataParameters * @param array $expectedParameters * @throws InceptionNotSuccessfulException */ + #[DataProvider('dataParameters')] public function testResolveParameters( string $configFile, array $expectedParameters, @@ -307,6 +305,10 @@ public function testResolveParameters( null, '0', false, + 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 07c6d9ffe2..4cab9de19b 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterIntegrationTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterIntegrationTest.php @@ -4,6 +4,7 @@ use Nette\Utils\Json; use PHPStan\ShouldNotHappenException; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use function array_sum; use function chdir; @@ -15,9 +16,7 @@ use function unlink; use const PHP_BINARY; -/** - * @group exec - */ +#[Group('exec')] class BaselineNeonErrorFormatterIntegrationTest extends TestCase { diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php index ac972f04a9..48b4a54e55 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php @@ -13,6 +13,7 @@ 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; @@ -28,7 +29,7 @@ class BaselineNeonErrorFormatterTest extends ErrorFormatterTestCase { - public function dataFormatterOutputProvider(): iterable + public static function dataFormatterOutputProvider(): iterable { yield [ 'No errors', @@ -112,10 +113,9 @@ public function dataFormatterOutputProvider(): iterable } /** - * @dataProvider dataFormatterOutputProvider - * * @param mixed[] $expected */ + #[DataProvider('dataFormatterOutputProvider')] public function testFormatErrors( string $message, int $exitCode, @@ -219,7 +219,7 @@ public function testEscapeDiNeon(): void /** * @return Generator}, void, void> */ - public function outputOrderingProvider(): Generator + public static function outputOrderingProvider(): Generator { $errors = [ new Error('Error #2', 'TestfileA', 1), @@ -240,9 +240,9 @@ 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)); @@ -314,7 +314,7 @@ public function testOutputOrdering(array $errors): void /** * @return Generator}> */ - public function endOfFileNewlinesProvider(): Generator + public static function endOfFileNewlinesProvider(): Generator { $existingBaselineContentWithoutEndNewlines = 'parameters: ignoreErrors: @@ -395,10 +395,9 @@ public function endOfFileNewlinesProvider(): Generator } /** - * @dataProvider endOfFileNewlinesProvider - * * @param list $errors */ + #[DataProvider('endOfFileNewlinesProvider')] public function testEndOfFileNewlines( array $errors, string $existingBaselineContent, @@ -424,7 +423,7 @@ public function testEndOfFileNewlines( if ($resource === false) { throw new ShouldNotHappenException(); } - $outputStream = new StreamOutput($resource, StreamOutput::VERBOSITY_NORMAL, false); + $outputStream = new StreamOutput($resource, decorated: false); $errorConsoleStyle = new ErrorsConsoleStyle(new StringInput(''), $outputStream); $output = new SymfonyOutput($outputStream, new SymfonyStyle($errorConsoleStyle)); @@ -438,14 +437,143 @@ public function testEndOfFileNewlines( rewind($outputStream->getStream()); $content = stream_get_contents($outputStream->getStream()); - if ($content === false) { - throw new ShouldNotHappenException(); - } - if ($expectedNewlinesCount > 0) { Assert::assertSame(str_repeat("\n", $expectedNewlinesCount), substr($content, -$expectedNewlinesCount)); } Assert::assertNotSame("\n", substr($content, -($expectedNewlinesCount + 1), 1)); } + public 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'), + ], + [ + 'parameters' => [ + 'ignoreErrors' => [ + [ + 'message' => '#^Foo$#', + 'count' => 2, + 'path' => 'Foo.php', + ], + [ + 'message' => '#^Foo with identifier$#', + 'identifier' => 'argument.type', + 'count' => 2, + 'path' => 'Foo.php', + ], + ], + ], + ], + ]; + + yield [ + [ + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + (new Error( + 'Foo with same message, different identifier', + __DIR__ . '/Foo.php', + 5, + ))->withIdentifier('argument.type'), + (new Error( + 'Foo with same message, different identifier', + __DIR__ . '/Foo.php', + 6, + ))->withIdentifier('argument.byRef'), + (new Error( + 'Foo with another message', + __DIR__ . '/Foo.php', + 5, + ))->withIdentifier('argument.type'), + ], + [ + 'parameters' => [ + 'ignoreErrors' => [ + [ + 'message' => '#^Foo$#', + 'count' => 2, + 'path' => 'Foo.php', + ], + [ + 'message' => '#^Foo with another message$#', + 'identifier' => 'argument.type', + 'count' => 1, + 'path' => 'Foo.php', + ], + [ + 'message' => '#^Foo with same message, different identifier$#', + 'identifier' => 'argument.byRef', + 'count' => 1, + 'path' => 'Foo.php', + ], + [ + 'message' => '#^Foo with same message, different identifier$#', + 'identifier' => 'argument.type', + 'count' => 1, + 'path' => 'Foo.php', + ], + ], + ], + ], + ]; + } + + /** + * @param list $errors + * @param mixed[] $expectedOutput + */ + #[DataProvider('dataFormatErrorsWithIdentifiers')] + public function testFormatErrorsWithIdentifiers(array $errors, array $expectedOutput): void + { + $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(__DIR__)); + $formatter->formatErrors( + new AnalysisResult( + $errors, + [], + [], + [], + [], + false, + null, + true, + 0, + true, + [], + ), + $this->getOutput(), + '', + ); + + $this->assertSame($expectedOutput, Neon::decode($this->getOutputContent())); + } + } diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php index b49c717f22..868154dd43 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php @@ -6,11 +6,12 @@ use PHPStan\Command\AnalysisResult; use PHPStan\File\ParentDirectoryRelativePathHelper; use PHPStan\Testing\ErrorFormatterTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class BaselinePhpErrorFormatterTest extends ErrorFormatterTestCase { - public function dataFormatErrors(): iterable + public static function dataFormatErrors(): iterable { yield [ [ @@ -80,8 +81,8 @@ public function dataFormatErrors(): iterable 'path' => __DIR__ . '/Foo.php', ]; \$ignoreErrors[] = [ - // identifier: argument.type 'message' => '#^Foo with identifier$#', + 'identifier' => 'argument.type', 'count' => 2, 'path' => __DIR__ . '/Foo.php', ]; @@ -127,15 +128,21 @@ public function dataFormatErrors(): iterable 'path' => __DIR__ . '/Foo.php', ]; \$ignoreErrors[] = [ - // identifier: argument.type 'message' => '#^Foo with another message$#', + 'identifier' => 'argument.type', 'count' => 1, 'path' => __DIR__ . '/Foo.php', ]; \$ignoreErrors[] = [ - // identifiers: argument.byRef, argument.type 'message' => '#^Foo with same message, different identifier$#', - 'count' => 2, + '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', ]; @@ -145,9 +152,9 @@ public function dataFormatErrors(): iterable } /** - * @dataProvider dataFormatErrors * @param list $errors */ + #[DataProvider('dataFormatErrors')] public function testFormatErrors(array $errors, string $expectedOutput): void { $formatter = new BaselinePhpErrorFormatter(new ParentDirectoryRelativePathHelper(__DIR__)); diff --git a/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php index 6618b6effe..48e906017c 100644 --- a/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php @@ -6,12 +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', @@ -110,10 +111,7 @@ public function dataFormatterOutputProvider(): iterable ]; } - /** - * @dataProvider dataFormatterOutputProvider - * - */ + #[DataProvider('dataFormatterOutputProvider')] public function testFormatErrors( string $message, int $exitCode, @@ -141,9 +139,8 @@ 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], @@ -172,9 +169,7 @@ public function testIdentifier(): void 'Foo', __DIR__ . '/FooTrait.php', 5, - true, - __DIR__ . '/Foo.php', - null, + filePath: __DIR__ . '/Foo.php', ))->withIdentifier('argument.type'); $formatter->formatErrors(new AnalysisResult( [$error], diff --git a/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php index 3a93977ce5..6af1b9c22b 100644 --- a/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php @@ -5,12 +5,13 @@ 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', @@ -75,10 +76,7 @@ public function dataFormatterOutputProvider(): iterable ]; } - /** - * @dataProvider dataFormatterOutputProvider - * - */ + #[DataProvider('dataFormatterOutputProvider')] public function testFormatErrors( string $message, int $exitCode, @@ -97,7 +95,7 @@ public function testFormatErrors( $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 78df3d79dd..8569f09fad 100644 --- a/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php @@ -4,12 +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', @@ -283,11 +284,7 @@ public function dataFormatterOutputProvider(): iterable ]; } - /** - * @dataProvider dataFormatterOutputProvider - * - * - */ + #[DataProvider('dataFormatterOutputProvider')] public function testFormatErrors( string $message, int $exitCode, diff --git a/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php index 9a1eca0188..19d05899f2 100644 --- a/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php @@ -6,12 +6,13 @@ 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', @@ -24,7 +25,7 @@ public function dataFormatterOutputProvider(): iterable "errors":0, "file_errors":0 }, - "files":[], + "files":{}, "errors": [] }', ]; @@ -67,7 +68,7 @@ public function dataFormatterOutputProvider(): iterable "errors":1, "file_errors":0 }, - "files":[], + "files":{}, "errors": [ "first generic error" ] @@ -133,7 +134,7 @@ public function dataFormatterOutputProvider(): iterable "errors":2, "file_errors":0 }, - "files":[], + "files":{}, "errors": [ "first generic error", "second generic" @@ -193,10 +194,7 @@ public function dataFormatterOutputProvider(): iterable ]; } - /** - * @dataProvider dataFormatterOutputProvider - * - */ + #[DataProvider('dataFormatterOutputProvider')] public function testPrettyFormatErrors( string $message, int $exitCode, @@ -215,11 +213,7 @@ public function testPrettyFormatErrors( $this->assertJsonStringEqualsJsonString($expected, $this->getOutputContent()); } - /** - * @dataProvider dataFormatterOutputProvider - * - * - */ + #[DataProvider('dataFormatterOutputProvider')] public function testFormatErrors( string $message, int $exitCode, @@ -238,21 +232,19 @@ public function testFormatErrors( $this->assertJsonStringEqualsJsonString($expected, $this->getOutputContent(), sprintf('%s: JSON do not match', $message)); } - public function dataFormatTip(): iterable + 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 - */ + #[DataProvider('dataFormatTip')] public function testFormatTip(string $tip, string $expectedTip): void { $formatter = new JsonErrorFormatter(false); $formatter->formatErrors(new AnalysisResult([ - new Error('Foo', '/foo/bar.php', 1, true, null, null, $tip), + new Error('Foo', '/foo/bar.php', 1, tip: $tip), ], [], [], [], [], false, null, true, 0, false, []), $this->getOutput()); $content = $this->getOutputContent(); diff --git a/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php index f83f162bb2..6c070585ac 100644 --- a/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php @@ -4,14 +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 { private JunitErrorFormatter $formatter; + #[Override] public function setUp(): void { parent::setUp(); @@ -22,7 +25,7 @@ public function setUp(): void /** * @return Generator> */ - public function dataFormatterOutputProvider(): Generator + public static function dataFormatterOutputProvider(): Generator { yield 'No errors' => [ 0, @@ -130,9 +133,8 @@ public function dataFormatterOutputProvider(): Generator /** * Test generated use cases for JUnit output format. - * - * @dataProvider dataFormatterOutputProvider */ + #[DataProvider('dataFormatterOutputProvider')] public function testFormatErrors( int $exitCode, int $numFileErrors, diff --git a/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php index ffe1c436d3..0781fdc73f 100644 --- a/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php @@ -3,12 +3,13 @@ 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 [ 'message' => 'No errors', @@ -102,9 +103,9 @@ public function dataFormatterOutputProvider(): iterable } /** - * @dataProvider dataFormatterOutputProvider * @param array{int, int}|int $numFileErrors */ + #[DataProvider('dataFormatterOutputProvider')] public function testFormatErrors( string $message, int $exitCode, @@ -121,7 +122,7 @@ public function testFormatErrors( $this->getOutput(false, $verbose), ), sprintf('%s: response code do not match', $message)); - $this->assertEquals($expected, $this->getOutputContent(false, $verbose), 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 85121ad337..eec2a3c7c2 100644 --- a/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php @@ -2,12 +2,14 @@ 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; @@ -15,18 +17,20 @@ class TableErrorFormatterTest extends ErrorFormatterTestCase { + #[Override] protected function setUp(): void { putenv('GITHUB_ACTIONS'); } + #[Override] protected function tearDown(): void { putenv('COLUMNS'); putenv('TERM_PROGRAM'); } - public function dataFormatterOutputProvider(): iterable + public static function dataFormatterOutputProvider(): iterable { yield [ 'message' => 'No errors', @@ -94,14 +98,14 @@ public function dataFormatterOutputProvider(): iterable 4 Foo ------ ------------------------------------------------------------------- - ------ ---------- + ------ ----------- Line foo.php - ------ ---------- + ------ ----------- 1 Foo 5 Bar Bar2 - 💡 a tip - ------ ---------- + 💡 a tip + ------ ----------- [ERROR] Found 4 errors @@ -143,14 +147,14 @@ public function dataFormatterOutputProvider(): iterable 4 Foo ------ ------------------------------------------------------------------- - ------ ---------- + ------ ----------- Line foo.php - ------ ---------- + ------ ----------- 1 Foo 5 Bar Bar2 - 💡 a tip - ------ ---------- + 💡 a tip + ------ ----------- -- ----------------------- Error @@ -190,12 +194,13 @@ public function dataFormatterOutputProvider(): iterable 'numGenericErrors' => 0, 'verbose' => false, 'extraEnvVars' => [], - 'expected' => ' ------ ------------ + 'expected' => ' ------ ---------------- Line foo.php - ------ ------------ + ------ ---------------- 5 Foobar\Buz - 💡 a tip - ------ ------------ + 🪪 foobar.buz + 💡 a tip + ------ ---------------- [ERROR] Found 1 error @@ -215,7 +220,7 @@ public function dataFormatterOutputProvider(): iterable ------ ---------------- 5 Foobar\Buz 🪪 foobar.buz - 💡 a tip + 💡 a tip ------ ---------------- @@ -226,10 +231,10 @@ public function dataFormatterOutputProvider(): iterable } /** - * @dataProvider dataFormatterOutputProvider * @param array{int, int}|int $numFileErrors * @param array $extraEnvVars */ + #[DataProvider('dataFormatterOutputProvider')] public function testFormatErrors( string $message, int $exitCode, @@ -252,13 +257,13 @@ public function testFormatErrors( $this->getOutput(false, $verbose), ), sprintf('%s: response code do not match', $message)); - $this->assertEquals($expected, $this->getOutputContent(false, $verbose), 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 = $this->createErrorFormatter('editor://%file%/%line%'); - $error = new Error('Test', 'Foo.php (in context of trait)', 12, true, 'Foo.php', 'Bar.php'); + $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()); @@ -271,7 +276,7 @@ public function testEditorUrlWithRelativePath(): void } $formatter = $this->createErrorFormatter('editor://custom/path/%relFile%/%line%'); - $error = new Error('Test', 'Foo.php', 12, true, self::DIRECTORY_PATH . '/rel/Foo.php'); + $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)); @@ -280,7 +285,7 @@ public function testEditorUrlWithRelativePath(): void public function testEditorUrlWithCustomTitle(): void { $formatter = $this->createErrorFormatter('editor://any', '%relFile%:%line%'); - $error = new Error('Test', 'Foo.php', 12, true, self::DIRECTORY_PATH . '/rel/Foo.php'); + $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)); @@ -315,6 +320,81 @@ public function testBug6727(): void 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, [], '/'); diff --git a/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php index 9e91634634..488d2ebcd6 100644 --- a/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php @@ -5,12 +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', @@ -18,6 +19,7 @@ public function dataFormatterOutputProvider(): iterable 0, 0, '', + '', ]; yield [ @@ -76,18 +78,29 @@ public function dataFormatterOutputProvider(): iterable ##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'foo.php\' line=\'5\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'a tip\'] ##teamcity[inspection typeId=\'phpstan\' message=\'first generic error\' file=\'.\' SEVERITY=\'ERROR\'] ##teamcity[inspection typeId=\'phpstan\' message=\'second generic\' file=\'.\' SEVERITY=\'ERROR\'] +', + ]; + + yield [ + 'One file error', + 1, + [4, 2], + 0, + '##teamcity[inspectionType id=\'phpstan\' name=\'phpstan\' category=\'phpstan\' description=\'phpstan Inspection\'] +##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'foo.php\' line=\'\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] +##teamcity[inspection typeId=\'phpstan\' message=\'Foobar\Buz (🪪 foobar.buz)\' file=\'foo.php\' line=\'5\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'a tip\'] ', ]; } /** - * @dataProvider dataFormatterOutputProvider - * + * @param array{int, int}|int $numFileErrors */ + #[DataProvider('dataFormatterOutputProvider')] public function testFormatErrors( string $message, int $exitCode, - int $numFileErrors, + array|int $numFileErrors, int $numGenericErrors, string $expected, ): void @@ -102,7 +115,7 @@ public function testFormatErrors( $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 98a6dc58cb..2b64d9e534 100644 --- a/tests/PHPStan/Command/IgnoredRegexValidatorTest.php +++ b/tests/PHPStan/Command/IgnoredRegexValidatorTest.php @@ -6,11 +6,12 @@ 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 [ [ @@ -100,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#', [], @@ -116,9 +153,9 @@ public function dataValidate(): array } /** - * @dataProvider dataValidate * @param string[] $expectedTypes */ + #[DataProvider('dataValidate')] public function testValidate( string $regex, array $expectedTypes, diff --git a/tests/PHPStan/Command/exclude-paths/full.neon b/tests/PHPStan/Command/exclude-paths/full.neon index bb813f036e..275024f3a0 100644 --- a/tests/PHPStan/Command/exclude-paths/full.neon +++ b/tests/PHPStan/Command/exclude-paths/full.neon @@ -3,4 +3,4 @@ parameters: analyse: - test analyseAndScan: - - test2 + - test2 (?) diff --git a/tests/PHPStan/Command/exclude-paths/including-mixed.neon b/tests/PHPStan/Command/exclude-paths/including-mixed.neon index d7b9f3ae38..5cb7be683d 100644 --- a/tests/PHPStan/Command/exclude-paths/including-mixed.neon +++ b/tests/PHPStan/Command/exclude-paths/including-mixed.neon @@ -6,4 +6,4 @@ parameters: analyse: - test analyseAndScan: - - test2 + - test2 (?) diff --git a/tests/PHPStan/Command/exclude-paths/including.neon b/tests/PHPStan/Command/exclude-paths/including.neon index 199397750d..058059038b 100644 --- a/tests/PHPStan/Command/exclude-paths/including.neon +++ b/tests/PHPStan/Command/exclude-paths/including.neon @@ -4,6 +4,6 @@ includes: parameters: excludePaths: analyse: - - test2 + - test2 (?) analyseAndScan: - - test3 + - test3 (?) diff --git a/tests/PHPStan/Command/exclude-paths/straightforward.neon b/tests/PHPStan/Command/exclude-paths/straightforward.neon index 2b5facc315..6a4a1bd0ab 100644 --- a/tests/PHPStan/Command/exclude-paths/straightforward.neon +++ b/tests/PHPStan/Command/exclude-paths/straightforward.neon @@ -1,4 +1,4 @@ parameters: excludePaths: - test - - test2 + - test2 (?) diff --git a/tests/PHPStan/Command/exclude-paths/test/.gitkeep b/tests/PHPStan/Command/exclude-paths/test/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/PHPStan/Command/relative-paths/nested/nested.neon b/tests/PHPStan/Command/relative-paths/nested/nested.neon index 6e097edf21..c004fd54b6 100644 --- a/tests/PHPStan/Command/relative-paths/nested/nested.neon +++ b/tests/PHPStan/Command/relative-paths/nested/nested.neon @@ -3,6 +3,7 @@ parameters: - here.php - test/there.php - ../up.php + reportUnmatchedIgnoredErrors: false ignoreErrors: - message: '#aaa#' diff --git a/tests/PHPStan/Command/relative-paths/root.neon b/tests/PHPStan/Command/relative-paths/root.neon index 834bcfd001..eb1330b4fc 100644 --- a/tests/PHPStan/Command/relative-paths/root.neon +++ b/tests/PHPStan/Command/relative-paths/root.neon @@ -11,7 +11,6 @@ parameters: - %rootDir%/conf paths: - src - memoryLimitFile: .memory_limit excludePaths: - src - src/*/data diff --git a/tests/PHPStan/Command/test-autodiscover-dist-dot-neon/phpstan.dist.neon b/tests/PHPStan/Command/test-autodiscover-dot-dist-dot-neon/.phpstan.dist.neon similarity index 100% rename from tests/PHPStan/Command/test-autodiscover-dist-dot-neon/phpstan.dist.neon rename to tests/PHPStan/Command/test-autodiscover-dot-dist-dot-neon/.phpstan.dist.neon diff --git a/tests/PHPStan/Command/test-autodiscover-dist/phpstan.neon.dist b/tests/PHPStan/Command/test-autodiscover-dot-dist/.phpstan.neon.dist similarity index 100% rename from tests/PHPStan/Command/test-autodiscover-dist/phpstan.neon.dist rename to tests/PHPStan/Command/test-autodiscover-dot-dist/.phpstan.neon.dist diff --git a/tests/PHPStan/Command/test-autodiscover/phpstan.neon b/tests/PHPStan/Command/test-autodiscover-dot/.phpstan.neon similarity index 100% rename from tests/PHPStan/Command/test-autodiscover/phpstan.neon rename to tests/PHPStan/Command/test-autodiscover-dot/.phpstan.neon diff --git a/tests/PHPStan/Command/test-autodiscover-no-dot-dist-dot-neon/phpstan.dist.neon b/tests/PHPStan/Command/test-autodiscover-no-dot-dist-dot-neon/phpstan.dist.neon new file mode 100644 index 0000000000..f242b77eeb --- /dev/null +++ b/tests/PHPStan/Command/test-autodiscover-no-dot-dist-dot-neon/phpstan.dist.neon @@ -0,0 +1,4 @@ +includes: + - ../../../../conf/bleedingEdge.neon + +parameters: diff --git a/tests/PHPStan/Command/test-autodiscover-no-dot-dist/phpstan.neon.dist b/tests/PHPStan/Command/test-autodiscover-no-dot-dist/phpstan.neon.dist new file mode 100644 index 0000000000..f242b77eeb --- /dev/null +++ b/tests/PHPStan/Command/test-autodiscover-no-dot-dist/phpstan.neon.dist @@ -0,0 +1,4 @@ +includes: + - ../../../../conf/bleedingEdge.neon + +parameters: diff --git a/tests/PHPStan/Command/test-autodiscover-no-dot/phpstan.neon b/tests/PHPStan/Command/test-autodiscover-no-dot/phpstan.neon new file mode 100644 index 0000000000..f242b77eeb --- /dev/null +++ b/tests/PHPStan/Command/test-autodiscover-no-dot/phpstan.neon @@ -0,0 +1,4 @@ +includes: + - ../../../../conf/bleedingEdge.neon + +parameters: diff --git a/tests/PHPStan/Command/test-autodiscover-priority-dist-dot-neon/.phpstan.dist.neon b/tests/PHPStan/Command/test-autodiscover-priority-dist-dot-neon/.phpstan.dist.neon new file mode 100644 index 0000000000..f242b77eeb --- /dev/null +++ b/tests/PHPStan/Command/test-autodiscover-priority-dist-dot-neon/.phpstan.dist.neon @@ -0,0 +1,4 @@ +includes: + - ../../../../conf/bleedingEdge.neon + +parameters: diff --git a/tests/PHPStan/Command/test-autodiscover-priority-dot/.phpstan.neon b/tests/PHPStan/Command/test-autodiscover-priority-dot/.phpstan.neon new file mode 100644 index 0000000000..f242b77eeb --- /dev/null +++ b/tests/PHPStan/Command/test-autodiscover-priority-dot/.phpstan.neon @@ -0,0 +1,4 @@ +includes: + - ../../../../conf/bleedingEdge.neon + +parameters: diff --git a/tests/PHPStan/Command/test-autodiscover-priority-dot/phpstan.neon b/tests/PHPStan/Command/test-autodiscover-priority-dot/phpstan.neon new file mode 100644 index 0000000000..f242b77eeb --- /dev/null +++ b/tests/PHPStan/Command/test-autodiscover-priority-dot/phpstan.neon @@ -0,0 +1,4 @@ +includes: + - ../../../../conf/bleedingEdge.neon + +parameters: diff --git a/tests/PHPStan/Composer/AutoloadFilesTest.php b/tests/PHPStan/Composer/AutoloadFilesTest.php index 889d37e1c7..3d735839e3 100644 --- a/tests/PHPStan/Composer/AutoloadFilesTest.php +++ b/tests/PHPStan/Composer/AutoloadFilesTest.php @@ -9,7 +9,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Finder\Finder; use function array_map; -use function array_splice; use function dirname; use function realpath; use function sort; @@ -17,7 +16,6 @@ use function strpos; use function substr; use const DIRECTORY_SEPARATOR; -use const PHP_VERSION_ID; class AutoloadFilesTest extends TestCase { @@ -62,25 +60,17 @@ public function testExpectedFiles(): void 'myclabs/deep-copy/src/DeepCopy/deep_copy.php', // dev dependency of PHPUnit 'react/async/src/functions_include.php', // added to phpstan-dist/bootstrap.php 'react/promise/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-php74/bootstrap.php', // afaik polyfills aren't necessary 'symfony/polyfill-php80/bootstrap.php', // afaik polyfills aren't necessary 'symfony/polyfill-php81/bootstrap.php', // afaik polyfills aren't necessary 'symfony/string/Resources/functions.php', // afaik polyfills aren't necessary ]; - $phpunitFunctions = 'phpunit/phpunit/src/Framework/Assert/Functions.php'; - if (PHP_VERSION_ID >= 70300) { - array_splice($expectedFiles, 6, 0, [ - $phpunitFunctions, - ]); - } - $expectedFiles = array_map(static fn (string $path): string => $fileHelper->normalizePath($path), $expectedFiles); sort($expectedFiles); diff --git a/tests/PHPStan/DependencyInjection/IgnoreErrorsTest.php b/tests/PHPStan/DependencyInjection/IgnoreErrorsTest.php index 112b256ed4..f24e65abc2 100644 --- a/tests/PHPStan/DependencyInjection/IgnoreErrorsTest.php +++ b/tests/PHPStan/DependencyInjection/IgnoreErrorsTest.php @@ -9,7 +9,7 @@ class IgnoreErrorsTest extends PHPStanTestCase public function testIgnoreErrors(): void { - $this->assertCount(12, self::getContainer()->getParameter('ignoreErrors')); + $this->assertCount(16, self::getContainer()->getParameter('ignoreErrors')); } /** 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 index 0d0af6673c..ded54328e9 100644 --- a/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabled.php +++ b/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabled.php @@ -2,7 +2,24 @@ namespace PHPStan\DependencyInjection; -class TestedConditionalServiceDisabled +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; + +/** + * @implements Rule + */ +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 index ffba6df404..636e786ea3 100644 --- a/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabledDisabled.php +++ b/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabledDisabled.php @@ -2,7 +2,24 @@ namespace PHPStan\DependencyInjection; -class TestedConditionalServiceDisabledDisabled +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; + +/** + * @implements Rule + */ +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 index 9234d1a4bd..e758ce8efe 100644 --- a/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabledEnabled.php +++ b/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabledEnabled.php @@ -2,7 +2,24 @@ namespace PHPStan\DependencyInjection; -class TestedConditionalServiceDisabledEnabled +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; + +/** + * @implements Rule + */ +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 index a267a71129..921f9a9590 100644 --- a/tests/PHPStan/DependencyInjection/TestedConditionalServiceEnabled.php +++ b/tests/PHPStan/DependencyInjection/TestedConditionalServiceEnabled.php @@ -2,7 +2,24 @@ namespace PHPStan\DependencyInjection; -class TestedConditionalServiceEnabled +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; + +/** + * @implements Rule + */ +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 index 9535501fb4..379700d04f 100644 --- a/tests/PHPStan/DependencyInjection/TestedConditionalServiceEnabledDisabled.php +++ b/tests/PHPStan/DependencyInjection/TestedConditionalServiceEnabledDisabled.php @@ -2,7 +2,24 @@ namespace PHPStan\DependencyInjection; -class TestedConditionalServiceEnabledDisabled +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; + +/** + * @implements Rule + */ +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 index 270201cc4e..d12d4121c8 100644 --- a/tests/PHPStan/DependencyInjection/TestedConditionalServiceEnabledEnabled.php +++ b/tests/PHPStan/DependencyInjection/TestedConditionalServiceEnabledEnabled.php @@ -2,7 +2,24 @@ namespace PHPStan\DependencyInjection; -class TestedConditionalServiceEnabledEnabled +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; + +/** + * @implements Rule + */ +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/ignoreErrors.neon b/tests/PHPStan/DependencyInjection/ignoreErrors.neon index df1d471d75..c137667bb4 100644 --- a/tests/PHPStan/DependencyInjection/ignoreErrors.neon +++ b/tests/PHPStan/DependencyInjection/ignoreErrors.neon @@ -45,3 +45,21 @@ parameters: 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/File/FileExcluderTest.php b/tests/PHPStan/File/FileExcluderTest.php index 7477416b6c..02aff99453 100644 --- a/tests/PHPStan/File/FileExcluderTest.php +++ b/tests/PHPStan/File/FileExcluderTest.php @@ -3,14 +3,15 @@ namespace PHPStan\File; use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class FileExcluderTest extends PHPStanTestCase { /** - * @dataProvider dataExcludeOnWindows * @param string[] $analyseExcludes */ + #[DataProvider('dataExcludeOnWindows')] public function testFilesAreExcludedFromAnalysingOnWindows( string $filePath, array $analyseExcludes, @@ -19,12 +20,12 @@ public function testFilesAreExcludedFromAnalysingOnWindows( { $this->skipIfNotOnWindows(); - $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes, false); + $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,16 +110,16 @@ public function dataExcludeOnWindows(): array ], [ 'c:\etc\phpstan-test\dummy-2.php', - ['c:\etc\phpstan'], + ['c:\etc\phpstan*'], true, ], ]; } /** - * @dataProvider dataExcludeOnUnix * @param string[] $analyseExcludes */ + #[DataProvider('dataExcludeOnUnix')] public function testFilesAreExcludedFromAnalysingOnUnix( string $filePath, array $analyseExcludes, @@ -127,12 +128,12 @@ public function testFilesAreExcludedFromAnalysingOnUnix( { $this->skipIfNotOnUnix(); - $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes, false); + $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes); $this->assertSame($isExcluded, $fileExcluder->isExcludedFromAnalysing($filePath)); } - public function dataExcludeOnUnix(): array + public static function dataExcludeOnUnix(): array { return [ [ @@ -142,7 +143,7 @@ public function dataExcludeOnUnix(): array ], [ __DIR__ . '/data/excluded-file.php', - [__DIR__], + [__DIR__ . '/*'], true, ], [ @@ -170,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'], @@ -192,7 +188,7 @@ public function dataExcludeOnUnix(): array ], [ '/etc/phpstan/dummy-1.php', - ['/etc/phpstan/'], + ['/etc/phpstan/*'], true, ], [ @@ -202,13 +198,13 @@ public function dataExcludeOnUnix(): array ], [ '/etc/phpstan-test/dummy-2.php', - ['/etc/phpstan'], + ['/etc/phpstan*'], true, ], ]; } - public function dataNoImplicitWildcard(): iterable + public static function dataNoImplicitWildcard(): iterable { yield [ __DIR__ . '/tests/foo.php', @@ -216,16 +212,6 @@ public function dataNoImplicitWildcard(): iterable __DIR__ . '/test', ], false, - true, - ]; - - yield [ - __DIR__ . '/tests/foo.php', - [ - __DIR__ . '/test', - ], - true, - false, ]; yield [ @@ -234,7 +220,6 @@ public function dataNoImplicitWildcard(): iterable __DIR__ . '/test', ], true, - true, ]; yield [ @@ -243,7 +228,6 @@ public function dataNoImplicitWildcard(): iterable __DIR__ . '/FileExcluderTest.php', ], true, - true, ]; yield [ @@ -252,24 +236,22 @@ public function dataNoImplicitWildcard(): iterable __DIR__ . '/test*', ], true, - true, ]; } /** - * @dataProvider dataNoImplicitWildcard * @param string[] $analyseExcludes */ + #[DataProvider('dataNoImplicitWildcard')] public function testNoImplicitWildcard( string $filePath, array $analyseExcludes, - bool $noImplicitWildcard, bool $isExcluded, ): void { $this->skipIfNotOnUnix(); - $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes, $noImplicitWildcard); + $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 409936a8d1..ac2cef5fd1 100644 --- a/tests/PHPStan/File/FileHelperTest.php +++ b/tests/PHPStan/File/FileHelperTest.php @@ -3,6 +3,7 @@ namespace PHPStan\File; use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class FileHelperTest extends PHPStanTestCase { @@ -10,7 +11,7 @@ class FileHelperTest extends PHPStanTestCase /** * @return string[][] */ - public function dataAbsolutizePathOnWindows(): array + public static function dataAbsolutizePathOnWindows(): array { return [ ['C:/Program Files', 'C:/Program Files'], @@ -25,9 +26,7 @@ public function dataAbsolutizePathOnWindows(): array ]; } - /** - * @dataProvider dataAbsolutizePathOnWindows - */ + #[DataProvider('dataAbsolutizePathOnWindows')] public function testAbsolutizePathOnWindows(string $path, string $absolutePath): void { $this->skipIfNotOnWindows(); @@ -38,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'], @@ -54,9 +53,7 @@ public function dataAbsolutizePathOnLinuxOrMac(): array ]; } - /** - * @dataProvider dataAbsolutizePathOnLinuxOrMac - */ + #[DataProvider('dataAbsolutizePathOnLinuxOrMac')] public function testAbsolutizePathOnLinuxOrMac(string $path, string $absolutePath): void { $this->skipIfNotOnUnix(); @@ -67,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'], @@ -81,9 +78,7 @@ public function dataNormalizePathOnWindows(): array ]; } - /** - * @dataProvider dataNormalizePathOnWindows - */ + #[DataProvider('dataNormalizePathOnWindows')] public function testNormalizePathOnWindows(string $path, string $normalizedPath): void { $this->skipIfNotOnWindows(); @@ -93,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'], @@ -109,9 +104,7 @@ public function dataNormalizePathOnLinuxOrMac(): array ]; } - /** - * @dataProvider dataNormalizePathOnLinuxOrMac - */ + #[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 d4065efb28..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,9 +104,7 @@ public function dataGetRelativePath(): array ]; } - /** - * @dataProvider dataGetRelativePath - */ + #[DataProvider('dataGetRelativePath')] public function testGetRelativePath( string $parentDirectory, string $filename, diff --git a/tests/PHPStan/File/RelativePathHelperTest.php b/tests/PHPStan/File/RelativePathHelperTest.php index 5d5b3eca41..7c484879cc 100644 --- a/tests/PHPStan/File/RelativePathHelperTest.php +++ b/tests/PHPStan/File/RelativePathHelperTest.php @@ -2,6 +2,7 @@ namespace PHPStan\File; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use function array_map; use function str_replace; @@ -10,7 +11,7 @@ class RelativePathHelperTest extends TestCase { - public function dataGetRelativePath(): array + public static function dataGetRelativePath(): array { return [ [ @@ -180,9 +181,9 @@ public function dataGetRelativePath(): array } /** - * @dataProvider dataGetRelativePath * @param string[] $analysedPaths */ + #[DataProvider('dataGetRelativePath')] public function testGetRelativePathOnUnix( string $currentWorkingDirectory, array $analysedPaths, @@ -198,9 +199,9 @@ public function testGetRelativePathOnUnix( } /** - * @dataProvider dataGetRelativePath * @param string[] $analysedPaths */ + #[DataProvider('dataGetRelativePath')] public function testGetRelativePathOnWindows( string $currentWorkingDirectory, array $analysedPaths, @@ -222,7 +223,7 @@ public function testGetRelativePathOnWindows( ); } - public function dataGetRelativePathWindowsSpecific(): array + public static function dataGetRelativePathWindowsSpecific(): array { return [ [ @@ -247,9 +248,9 @@ public function dataGetRelativePathWindowsSpecific(): array } /** - * @dataProvider dataGetRelativePathWindowsSpecific * @param string[] $analysedPaths */ + #[DataProvider('dataGetRelativePathWindowsSpecific')] public function testGetRelativePathWindowsSpecific( string $currentWorkingDirectory, array $analysedPaths, diff --git a/tests/PHPStan/Generics/GenericsIntegrationTest.php b/tests/PHPStan/Generics/GenericsIntegrationTest.php index 6f25d37cd0..e8309a0846 100644 --- a/tests/PHPStan/Generics/GenericsIntegrationTest.php +++ b/tests/PHPStan/Generics/GenericsIntegrationTest.php @@ -3,14 +3,13 @@ namespace PHPStan\Generics; use PHPStan\Testing\LevelsTestCase; +use PHPUnit\Framework\Attributes\Group; -/** - * @group levels - */ +#[Group('levels')] class GenericsIntegrationTest extends LevelsTestCase { - public function dataTopics(): array + public static function dataTopics(): array { return [ ['functions'], diff --git a/tests/PHPStan/Generics/TemplateTypeFactoryTest.php b/tests/PHPStan/Generics/TemplateTypeFactoryTest.php index 16fe1d24df..20be5000fc 100644 --- a/tests/PHPStan/Generics/TemplateTypeFactoryTest.php +++ b/tests/PHPStan/Generics/TemplateTypeFactoryTest.php @@ -8,19 +8,21 @@ 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 PHPStanTestCase { /** @return array */ - public function dataCreate(): array + public static function dataCreate(): array { return [ [ @@ -54,7 +56,12 @@ public function dataCreate(): array null, TemplateTypeVariance::createInvariant(), ), - new MixedType(), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'U', + null, + TemplateTypeVariance::createInvariant(), + ), ], [ new UnionType([ @@ -66,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'); diff --git a/tests/PHPStan/Generics/data/typeProjections.php b/tests/PHPStan/Generics/data/typeProjections.php index 8cfe1f92db..555c1a50ae 100644 --- a/tests/PHPStan/Generics/data/typeProjections.php +++ b/tests/PHPStan/Generics/data/typeProjections.php @@ -1,4 +1,4 @@ -= 7.4 +, 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 df0dd443c2..3338348583 100644 --- a/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php +++ b/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php @@ -3,14 +3,13 @@ namespace PHPStan\Levels; use PHPStan\Testing\LevelsTestCase; +use PHPUnit\Framework\Attributes\Group; -/** - * @group levels - */ +#[Group('levels')] class InferPrivatePropertyTypeFromConstructorIntegrationTest extends LevelsTestCase { - public function dataTopics(): array + public static function dataTopics(): array { return [ ['inferPropertyType'], diff --git a/tests/PHPStan/Levels/LevelsIntegrationTest.php b/tests/PHPStan/Levels/LevelsIntegrationTest.php index 45f5d7634e..12499dce44 100644 --- a/tests/PHPStan/Levels/LevelsIntegrationTest.php +++ b/tests/PHPStan/Levels/LevelsIntegrationTest.php @@ -3,15 +3,14 @@ namespace PHPStan\Levels; use PHPStan\Testing\LevelsTestCase; +use PHPUnit\Framework\Attributes\Group; use const PHP_VERSION_ID; -/** - * @group levels - */ +#[Group('levels')] class LevelsIntegrationTest extends LevelsTestCase { - public function dataTopics(): array + public static function dataTopics(): array { $topics = [ ['returnTypes'], @@ -41,6 +40,7 @@ public function dataTopics(): array ['coalesce'], ['arrayDestructuring'], ['listType'], + ['missingTypes'], ]; if (PHP_VERSION_ID >= 80300) { $topics[] = ['constantAccesses83']; diff --git a/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php b/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php index 854d9ded9e..999427f6c6 100644 --- a/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php +++ b/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php @@ -3,14 +3,13 @@ namespace PHPStan\Levels; use PHPStan\Testing\LevelsTestCase; +use PHPUnit\Framework\Attributes\Group; -/** - * @group levels - */ +#[Group('levels')] class NamedArgumentsIntegrationTest extends LevelsTestCase { - public function dataTopics(): array + public static function dataTopics(): array { return [ ['namedArguments'], diff --git a/tests/PHPStan/Levels/StubValidatorIntegrationTest.php b/tests/PHPStan/Levels/StubValidatorIntegrationTest.php index f4138d043f..d76d9045c0 100644 --- a/tests/PHPStan/Levels/StubValidatorIntegrationTest.php +++ b/tests/PHPStan/Levels/StubValidatorIntegrationTest.php @@ -3,14 +3,13 @@ namespace PHPStan\Levels; use PHPStan\Testing\LevelsTestCase; +use PHPUnit\Framework\Attributes\Group; -/** - * @group levels - */ +#[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 dfec2e90f5..c8be90cd53 100644 --- a/tests/PHPStan/Levels/StubsIntegrationTest.php +++ b/tests/PHPStan/Levels/StubsIntegrationTest.php @@ -3,14 +3,13 @@ namespace PHPStan\Levels; use PHPStan\Testing\LevelsTestCase; +use PHPUnit\Framework\Attributes\Group; -/** - * @group levels - */ +#[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/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-5.json b/tests/PHPStan/Levels/data/acceptTypes-5.json index 88d4749a45..76aac5c080 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-5.json +++ b/tests/PHPStan/Levels/data/acceptTypes-5.json @@ -180,7 +180,7 @@ "ignorable": true }, { - "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array{} given.", + "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array{} given.", "line": 733, "ignorable": true }, @@ -189,4 +189,4 @@ "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 9e0afc2f64..ba67e6a9d6 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-7.json +++ b/tests/PHPStan/Levels/data/acceptTypes-7.json @@ -85,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 }, @@ -95,12 +95,12 @@ "ignorable": true }, { - "message": "Parameter #1 $array of method Levels\\AcceptTypes\\Baz::requireArray() expects array, array|Levels\\AcceptTypes\\Foo given.", + "message": "Parameter #1 $array of method Levels\\AcceptTypes\\Baz::requireArray() expects array, array|Levels\\AcceptTypes\\Foo given.", "line": 542, "ignorable": true }, { - "message": "Parameter #1 $foo of method Levels\\AcceptTypes\\Baz::requireFoo() expects Levels\\AcceptTypes\\Foo, array|Levels\\AcceptTypes\\Foo given.", + "message": "Parameter #1 $foo of method Levels\\AcceptTypes\\Baz::requireFoo() expects Levels\\AcceptTypes\\Foo, array|Levels\\AcceptTypes\\Foo given.", "line": 543, "ignorable": true }, @@ -160,7 +160,7 @@ "ignorable": true }, { - "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array given.", + "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array given.", "line": 735, "ignorable": true }, @@ -169,4 +169,4 @@ "line": 756, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/arrayAccess-10.json b/tests/PHPStan/Levels/data/arrayAccess-10.json new file mode 100644 index 0000000000..9dc3ca3eb9 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayAccess-10.json @@ -0,0 +1,7 @@ +[ + { + "message": "Cannot assign offset mixed to SplObjectStorage.", + "line": 43, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayDestructuring-8.json b/tests/PHPStan/Levels/data/arrayDestructuring-8.json index 7842bce806..3b033abd6d 100644 --- a/tests/PHPStan/Levels/data/arrayDestructuring-8.json +++ b/tests/PHPStan/Levels/data/arrayDestructuring-8.json @@ -1,7 +1,7 @@ [ { - "message": "Cannot use array destructuring on array|null.", + "message": "Cannot use array destructuring on array|null.", "line": 15, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/arrayDimFetches-10.json b/tests/PHPStan/Levels/data/arrayDimFetches-10.json new file mode 100644 index 0000000000..900a4ee636 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayDimFetches-10.json @@ -0,0 +1,22 @@ +[ + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 14, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 21, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 27, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 28, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayDimFetches-8.json b/tests/PHPStan/Levels/data/arrayDimFetches-8.json index 07568e3768..e7e6efcd13 100644 --- a/tests/PHPStan/Levels/data/arrayDimFetches-8.json +++ b/tests/PHPStan/Levels/data/arrayDimFetches-8.json @@ -1,6 +1,6 @@ [ { - "message": "Offset 0 might not exist on array|null.", + "message": "Offset 0 might not exist on array|null.", "line": 15, "ignorable": true }, @@ -9,4 +9,4 @@ "line": 50, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/callableCalls-10-missing.json b/tests/PHPStan/Levels/data/callableCalls-10-missing.json new file mode 100644 index 0000000000..5c7f12b38d --- /dev/null +++ b/tests/PHPStan/Levels/data/callableCalls-10-missing.json @@ -0,0 +1,12 @@ +[ + { + "message": "Closure invoked with 0 parameters, 1 required.", + "line": 37, + "ignorable": true + }, + { + "message": "Trying to invoke int but it's not a callable.", + "line": 43, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/casts-7.json b/tests/PHPStan/Levels/data/casts-7.json index d9b7a85b78..e2810b0e42 100644 --- a/tests/PHPStan/Levels/data/casts-7.json +++ b/tests/PHPStan/Levels/data/casts-7.json @@ -1,12 +1,12 @@ [ { - "message": "Cannot cast array|(callable(): mixed) to int.", + "message": "Cannot cast array|(callable(): mixed) to int.", "line": 20, "ignorable": true }, { - "message": "Cannot cast array|float|int to string.", + "message": "Cannot cast array|float|int to string.", "line": 21, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/clone-10-missing.json b/tests/PHPStan/Levels/data/clone-10-missing.json new file mode 100644 index 0000000000..40e1203120 --- /dev/null +++ b/tests/PHPStan/Levels/data/clone-10-missing.json @@ -0,0 +1,12 @@ +[ + { + "message": "Cannot clone non-object variable $nullableInt of type int.", + "line": 34, + "ignorable": true + }, + { + "message": "Cannot clone non-object variable $nullableUnion of type int|Levels\\Cloning\\Foo.", + "line": 35, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/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/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 index e36b08fc79..dae088dd65 100644 --- a/tests/PHPStan/Levels/data/constantAccesses83-2.json +++ b/tests/PHPStan/Levels/data/constantAccesses83-2.json @@ -1,7 +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 } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/constantAccesses83-7.json b/tests/PHPStan/Levels/data/constantAccesses83-7.json index 3dd562d1b3..6bca19187e 100644 --- a/tests/PHPStan/Levels/data/constantAccesses83-7.json +++ b/tests/PHPStan/Levels/data/constantAccesses83-7.json @@ -1,4 +1,9 @@ [ + { + "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, diff --git a/tests/PHPStan/Levels/data/constantAccesses83-8.json b/tests/PHPStan/Levels/data/constantAccesses83-8.json index e9c93a1ee7..f6136344ba 100644 --- a/tests/PHPStan/Levels/data/constantAccesses83-8.json +++ b/tests/PHPStan/Levels/data/constantAccesses83-8.json @@ -1,4 +1,9 @@ [ + { + "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, 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/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-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/stubValidator-0.json b/tests/PHPStan/Levels/data/stubValidator-0.json index 0517d298af..ce13d6e997 100644 --- a/tests/PHPStan/Levels/data/stubValidator-0.json +++ b/tests/PHPStan/Levels/data/stubValidator-0.json @@ -20,12 +20,12 @@ "ignorable": false }, { - "message": "Method class@anonymous/stubValidator/stubs.php:27::doFoo() has no return type specified.", + "message": "Method ArrayIterator@anonymous/stubValidator/stubs.php:27::doFoo() has no return type specified.", "line": 30, "ignorable": false }, { - "message": "Parameter $foo of method class@anonymous/stubValidator/stubs.php:27::doFoo() has invalid type StubValidator\\Foooooooo.", + "message": "Parameter $foo of method ArrayIterator@anonymous/stubValidator/stubs.php:27::doFoo() has invalid type StubValidator\\Foooooooo.", "line": 30, "ignorable": false } diff --git a/tests/PHPStan/Levels/data/stubs-functions-4.json b/tests/PHPStan/Levels/data/stubs-functions-4.json new file mode 100644 index 0000000000..dd57bdf13f --- /dev/null +++ b/tests/PHPStan/Levels/data/stubs-functions-4.json @@ -0,0 +1,12 @@ +[ + { + "message": "Call to function StubsIntegrationTest\\foo() on a separate line has no effect.", + "line": 11, + "ignorable": true + }, + { + "message": "Call to function StubsIntegrationTest\\foo() on a separate line has no effect.", + "line": 13, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/unreachable-0.json b/tests/PHPStan/Levels/data/unreachable-0.json deleted file mode 100644 index 5091fec153..0000000000 --- a/tests/PHPStan/Levels/data/unreachable-0.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "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": "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 - } -] diff --git a/tests/PHPStan/Levels/data/unreachable-4.json b/tests/PHPStan/Levels/data/unreachable-4.json index 6f937312ae..4e6216b466 100644 --- a/tests/PHPStan/Levels/data/unreachable-4.json +++ b/tests/PHPStan/Levels/data/unreachable-4.json @@ -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, @@ -64,6 +69,11 @@ "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, 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/Node/AttributeArgRuleTest.php b/tests/PHPStan/Node/AttributeArgRuleTest.php index fb4c1d99e9..796d5d3bdb 100644 --- a/tests/PHPStan/Node/AttributeArgRuleTest.php +++ b/tests/PHPStan/Node/AttributeArgRuleTest.php @@ -5,7 +5,8 @@ use PhpParser\Node; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -21,7 +22,7 @@ protected function getRule(): Rule return new AttributeArgRule(); } - public function dataRule(): iterable + public static function dataRule(): iterable { yield [ __DIR__ . '/data/attributes.php', @@ -32,8 +33,8 @@ public function dataRule(): iterable /** * @param int[] $lines - * @dataProvider dataRule */ + #[DataProvider('dataRule')] public function testRule(string $file, string $expectedError, array $lines): void { $errors = []; @@ -43,12 +44,9 @@ public function testRule(string $file, string $expectedError, array $lines): voi $this->analyse([$file], $errors); } + #[RequiresPhp('>= 8.1')] public function testEnumCaseAttribute(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/enum-case-attribute.php'], [ [ AttributeArgRule::ERROR_MESSAGE, diff --git a/tests/PHPStan/Node/AttributeGroupRuleTest.php b/tests/PHPStan/Node/AttributeGroupRuleTest.php index d1e3044f44..67141a33b2 100644 --- a/tests/PHPStan/Node/AttributeGroupRuleTest.php +++ b/tests/PHPStan/Node/AttributeGroupRuleTest.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; /** * @extends RuleTestCase @@ -20,7 +21,7 @@ protected function getRule(): Rule return new AttributeGroupRule(); } - public function dataRule(): iterable + public static function dataRule(): iterable { yield [ __DIR__ . '/data/attributes.php', @@ -31,8 +32,8 @@ public function dataRule(): iterable /** * @param int[] $lines - * @dataProvider dataRule */ + #[DataProvider('dataRule')] public function testRule(string $file, string $expectedError, array $lines): void { $errors = []; diff --git a/tests/PHPStan/Node/AttributeRuleTest.php b/tests/PHPStan/Node/AttributeRuleTest.php index 3b189a210d..135a0ca20d 100644 --- a/tests/PHPStan/Node/AttributeRuleTest.php +++ b/tests/PHPStan/Node/AttributeRuleTest.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; /** * @extends RuleTestCase @@ -20,7 +21,7 @@ protected function getRule(): Rule return new AttributeRule(); } - public function dataRule(): iterable + public static function dataRule(): iterable { yield [ __DIR__ . '/data/attributes.php', @@ -31,8 +32,8 @@ public function dataRule(): iterable /** * @param int[] $lines - * @dataProvider dataRule */ + #[DataProvider('dataRule')] public function testRule(string $file, string $expectedError, array $lines): void { $errors = []; diff --git a/tests/PHPStan/Node/FileNodeTest.php b/tests/PHPStan/Node/FileNodeTest.php index 60a132a7f8..aebcd787be 100644 --- a/tests/PHPStan/Node/FileNodeTest.php +++ b/tests/PHPStan/Node/FileNodeTest.php @@ -9,6 +9,7 @@ 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; @@ -51,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array }; } - public function dataRule(): iterable + public static function dataRule(): iterable { yield [ __DIR__ . '/data/empty.php', @@ -72,9 +73,7 @@ public function dataRule(): iterable ]; } - /** - * @dataProvider dataRule - */ + #[DataProvider('dataRule')] public function testRule(string $file, string $expectedError, int $line): void { $this->analyse([$file], [ diff --git a/tests/PHPStan/Parallel/ParallelAnalyserIntegrationTest.php b/tests/PHPStan/Parallel/ParallelAnalyserIntegrationTest.php index 3ae92e09dc..fb60543a17 100644 --- a/tests/PHPStan/Parallel/ParallelAnalyserIntegrationTest.php +++ b/tests/PHPStan/Parallel/ParallelAnalyserIntegrationTest.php @@ -2,24 +2,29 @@ namespace PHPStan\Parallel; +use Nette\Utils\FileSystem; use Nette\Utils\Json; use PHPStan\File\FileHelper; use PHPStan\ShouldNotHappenException; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use function array_map; use function escapeshellarg; use function exec; use function implode; +use function md5; +use function putenv; use function sprintf; +use function sys_get_temp_dir; +use function uniqid; use const PHP_BINARY; -/** - * @group exec - */ +#[Group('exec')] class ParallelAnalyserIntegrationTest extends TestCase { - public function dataRun(): array + public static function dataRun(): array { return [ ['analyse'], @@ -27,11 +32,11 @@ public function dataRun(): array ]; } - /** - * @dataProvider dataRun - */ + #[DataProvider('dataRun')] public function testRun(string $command): void { + $tmpDir = sys_get_temp_dir() . '/' . md5(uniqid()); + putenv('PHPSTAN_TMP_DIR=' . $tmpDir); exec(sprintf('%s %s clear-result-cache --configuration %s -q', escapeshellarg(PHP_BINARY), escapeshellarg(__DIR__ . '/../../../bin/phpstan'), escapeshellarg(__DIR__ . '/parallel-analyser.neon')), $clearResultCacheOutputLines, $clearResultCacheExitCode); if ($clearResultCacheExitCode !== 0) { throw new ShouldNotHappenException('Could not clear result cache.'); @@ -50,6 +55,8 @@ public function testRun(string $command): void ), $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([ diff --git a/tests/PHPStan/Parallel/SchedulerTest.php b/tests/PHPStan/Parallel/SchedulerTest.php index fb1fd626cf..284af8d55c 100644 --- a/tests/PHPStan/Parallel/SchedulerTest.php +++ b/tests/PHPStan/Parallel/SchedulerTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Parallel; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use function array_fill; use function array_map; @@ -10,7 +11,7 @@ class SchedulerTest extends TestCase { - public function dataSchedule(): array + public static function dataSchedule(): array { return [ [ @@ -71,13 +72,13 @@ public function dataSchedule(): array } /** - * @dataProvider dataSchedule * @param positive-int $jobSize * @param positive-int $maximumNumberOfProcesses * @param positive-int $minimumNumberOfJobsPerProcess * @param 0|positive-int $numberOfFiles * @param array $expectedJobSizes */ + #[DataProvider('dataSchedule')] public function testSchedule( int $cpuCores, int $maximumNumberOfProcesses, 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 13505dbce7..a0df7a1b2a 100644 --- a/tests/PHPStan/Parser/CachedParserTest.php +++ b/tests/PHPStan/Parser/CachedParserTest.php @@ -8,14 +8,13 @@ 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 - */ + #[DataProvider('dataParseFileClearCache')] public function testParseFileClearCache( int $cachedNodesByStringCountMax, int $cachedNodesByStringCountExpected, @@ -26,7 +25,7 @@ public function testParseFileClearCache( $cachedNodesByStringCountMax, ); - $this->assertEquals( + $this->assertSame( $cachedNodesByStringCountMax, $parser->getCachedNodesByStringCountMax(), ); @@ -36,7 +35,7 @@ public function testParseFileClearCache( $parser->parseString('string' . $i); } - $this->assertEquals( + $this->assertSame( $cachedNodesByStringCountExpected, $parser->getCachedNodesByStringCount(), ); @@ -47,7 +46,10 @@ public function testParseFileClearCache( ); } - public function dataParseFileClearCache(): Generator + /** + * @return Generator + */ + public static function dataParseFileClearCache(): Generator { yield 'even' => [ 'cachedNodesByStringCountMax' => 50, @@ -83,6 +85,7 @@ public function testParseTheSameFileWithDifferentMethod(): void self::getContainer()->getService('currentPhpVersionRichParser'), self::getContainer()->getService('currentPhpVersionSimpleDirectParser'), self::getContainer()->getService('php8Parser'), + null, ); $parser = new CachedParser($pathRoutingParser, 500); $path = $fileHelper->normalizePath(__DIR__ . '/data/test.php'); @@ -90,15 +93,34 @@ public function testParseTheSameFileWithDifferentMethod(): void $contents = FileReader::read($path); $stmts = $parser->parseString($contents); $this->assertInstanceOf(Namespace_::class, $stmts[0]); - $this->assertNull($stmts[0]->stmts[0]->getAttribute('parent')); + $this->assertInstanceOf(Node\Stmt\Expression::class, $stmts[0]->stmts[0]); + $this->assertInstanceOf(Node\Expr\Assign::class, $stmts[0]->stmts[0]->expr); + $this->assertInstanceOf(Node\Expr\New_::class, $stmts[0]->stmts[0]->expr->expr); + $this->assertNull($stmts[0]->stmts[0]->expr->expr->class->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX)); $stmts = $parser->parseFile($path); $this->assertInstanceOf(Namespace_::class, $stmts[0]); - $this->assertInstanceOf(Namespace_::class, $stmts[0]->stmts[0]->getAttribute('parent')); + $this->assertInstanceOf(Node\Stmt\Expression::class, $stmts[0]->stmts[0]); + $this->assertInstanceOf(Node\Expr\Assign::class, $stmts[0]->stmts[0]->expr); + $this->assertInstanceOf(Node\Expr\New_::class, $stmts[0]->stmts[0]->expr->expr); + $this->assertSame(1, $stmts[0]->stmts[0]->expr->expr->class->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX)); + + $this->assertInstanceOf(Node\Stmt\Expression::class, $stmts[0]->stmts[1]); + $this->assertInstanceOf(Node\Expr\Assign::class, $stmts[0]->stmts[1]->expr); + $this->assertInstanceOf(Node\Expr\New_::class, $stmts[0]->stmts[1]->expr->expr); + $this->assertSame(2, $stmts[0]->stmts[1]->expr->expr->class->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX)); $stmts = $parser->parseString($contents); $this->assertInstanceOf(Namespace_::class, $stmts[0]); - $this->assertInstanceOf(Namespace_::class, $stmts[0]->stmts[0]->getAttribute('parent')); + $this->assertInstanceOf(Node\Stmt\Expression::class, $stmts[0]->stmts[0]); + $this->assertInstanceOf(Node\Expr\Assign::class, $stmts[0]->stmts[0]->expr); + $this->assertInstanceOf(Node\Expr\New_::class, $stmts[0]->stmts[0]->expr->expr); + $this->assertSame(1, $stmts[0]->stmts[0]->expr->expr->class->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX)); + + $this->assertInstanceOf(Node\Stmt\Expression::class, $stmts[0]->stmts[1]); + $this->assertInstanceOf(Node\Expr\Assign::class, $stmts[0]->stmts[1]->expr); + $this->assertInstanceOf(Node\Expr\New_::class, $stmts[0]->stmts[1]->expr->expr); + $this->assertSame(2, $stmts[0]->stmts[1]->expr->expr->class->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX)); } } diff --git a/tests/PHPStan/Parser/CleaningParserTest.php b/tests/PHPStan/Parser/CleaningParserTest.php index 692ea7b699..c94558ed89 100644 --- a/tests/PHPStan/Parser/CleaningParserTest.php +++ b/tests/PHPStan/Parser/CleaningParserTest.php @@ -4,17 +4,18 @@ use PhpParser\Lexer\Emulative; use PhpParser\NodeVisitor\NameResolver; -use PhpParser\Parser\Php7; +use PhpParser\Parser\Php8; use PHPStan\File\FileReader; use PHPStan\Node\Printer\Printer; use PHPStan\Php\PhpVersion; use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; use const PHP_VERSION_ID; class CleaningParserTest extends PHPStanTestCase { - public function dataParse(): iterable + public static function dataParse(): iterable { return [ [ @@ -52,12 +53,15 @@ public function dataParse(): iterable __DIR__ . '/data/cleaning-php-version-after-74.php', 70400, ], + [ + __DIR__ . '/data/cleaning-property-hooks-before.php', + __DIR__ . '/data/cleaning-property-hooks-after.php', + 80400, + ], ]; } - /** - * @dataProvider dataParse - */ + #[DataProvider('dataParse')] public function testParse( string $beforeFile, string $afterFile, @@ -66,8 +70,10 @@ public function testParse( { $parser = new CleaningParser( new SimpleParser( - new Php7(new Emulative()), + new Php8(new Emulative()), new NameResolver(), + new VariadicMethodsVisitor(), + new VariadicFunctionsVisitor(), ), new PhpVersion($phpVersionId), ); 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 index 103eb129b4..a821dbdb5c 100644 --- a/tests/PHPStan/Parser/RichParserTest.php +++ b/tests/PHPStan/Parser/RichParserTest.php @@ -3,12 +3,13 @@ namespace PHPStan\Parser; use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; use const PHP_EOL; class RichParserTest extends PHPStanTestCase { - public function dataLinesToIgnore(): iterable + public static function dataLinesToIgnore(): iterable { yield [ ' ['test'], + 4 => ['test'], + ], + ]; + + yield [ + ' ['test'], ], ]; @@ -261,12 +272,51 @@ public function dataLinesToIgnore(): iterable 2 => ['identifier'], ], ]; + + yield [ + 'myProperty = $b;' . PHP_EOL . + ' }' . PHP_EOL . + '}', + [ + 10 => ['variable.undefined'], + ], + ]; + + yield [ + 'myProperty = $b;' . PHP_EOL . + ' }' . PHP_EOL . + '}', + [ + 13 => ['variable.undefined'], + ], + ]; } /** - * @dataProvider dataLinesToIgnore * @param array|null> $expectedLines */ + #[DataProvider('dataLinesToIgnore')] public function testLinesToIgnore(string $code, array $expectedLines): void { /** @var RichParser $parser */ @@ -277,7 +327,7 @@ public function testLinesToIgnore(string $code, array $expectedLines): void $this->assertSame($expectedLines, $lines); } - public function dataLinesToIgnoreParseErrors(): iterable + public static function dataLinesToIgnoreParseErrors(): iterable { yield [ '> $expectedErrors */ + #[DataProvider('dataLinesToIgnoreParseErrors')] public function testLinesToIgnoreParseErrors(string $code, array $expectedErrors): void { /** @var RichParser $parser */ diff --git a/tests/PHPStan/Parser/data/cleaning-property-hooks-after.php b/tests/PHPStan/Parser/data/cleaning-property-hooks-after.php new file mode 100644 index 0000000000..105bcf5d76 --- /dev/null +++ b/tests/PHPStan/Parser/data/cleaning-property-hooks-after.php @@ -0,0 +1,21 @@ +i; + } + } +} +class FooParam +{ + public function __construct(public int $i { + get { + $this->i; + } + }) + { + } +} diff --git a/tests/PHPStan/Parser/data/cleaning-property-hooks-before.php b/tests/PHPStan/Parser/data/cleaning-property-hooks-before.php new file mode 100644 index 0000000000..7b73ef3d09 --- /dev/null +++ b/tests/PHPStan/Parser/data/cleaning-property-hooks-before.php @@ -0,0 +1,42 @@ +j; + + // backed property, leave this here + return $this->i; + } + } + +} + +class FooParam +{ + + public function __construct( + public int $i { + get { + echo 'irrelevant'; + + // other property, clean up + echo $this->j; + + // backed property, leave this here + return $this->i; + } + } + ) + { + + } + +} diff --git a/tests/PHPStan/Parser/data/test.php b/tests/PHPStan/Parser/data/test.php index a6bee51214..b7ee628d37 100644 --- a/tests/PHPStan/Parser/data/test.php +++ b/tests/PHPStan/Parser/data/test.php @@ -2,7 +2,4 @@ namespace CachedParserBug; -class Foo -{ - -} +$a = new class () {}; $b = new class () {}; diff --git a/tests/PHPStan/Parser/data/variadic-functions.php b/tests/PHPStan/Parser/data/variadic-functions.php new file mode 100644 index 0000000000..d1a572e1e0 --- /dev/null +++ b/tests/PHPStan/Parser/data/variadic-functions.php @@ -0,0 +1,31 @@ += 8.1 + +namespace VariadicMethodEnum; + +enum X { + + function non_variadic_fn1($v) { + } + + function variadic_fn1(...$v) { + } + + function implicit_variadic_fn1() { + $args = func_get_args(); + } +} diff --git a/tests/PHPStan/Parser/data/variadic-methods.php b/tests/PHPStan/Parser/data/variadic-methods.php new file mode 100644 index 0000000000..da6135b967 --- /dev/null +++ b/tests/PHPStan/Parser/data/variadic-methods.php @@ -0,0 +1,68 @@ +assertSame( + $expected->describe(), + $phpVersions->producesWarningForFinalPrivateMethods()->describe(), + ); + } + + public 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 index 03218eff04..90635f3e69 100644 --- a/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php +++ b/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php @@ -2,6 +2,7 @@ namespace PHPStan\PhpDoc; +use Override; use PHPStan\Testing\PHPStanTestCase; use function sprintf; @@ -10,6 +11,7 @@ class DefaultStubFilesProviderTest extends PHPStanTestCase private string $currentWorkingDirectory; + #[Override] protected function setUp(): void { $this->currentWorkingDirectory = $this->getContainer()->getParameter('currentWorkingDirectory'); diff --git a/tests/PHPStan/PhpDoc/TypeDescriptionTest.php b/tests/PHPStan/PhpDoc/TypeDescriptionTest.php index 29c3a35231..e7147bb077 100644 --- a/tests/PHPStan/PhpDoc/TypeDescriptionTest.php +++ b/tests/PHPStan/PhpDoc/TypeDescriptionTest.php @@ -19,12 +19,13 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; use function sprintf; class TypeDescriptionTest extends PHPStanTestCase { - public function dataTest(): iterable + public static function dataTest(): iterable { yield ['string', new StringType()]; yield ['array', new ArrayType(new MixedType(), new MixedType())]; @@ -69,9 +70,7 @@ public function dataTest(): iterable yield ['array{\'"foo"\': int}', $builder->getArray()]; } - /** - * @dataProvider dataTest - */ + #[DataProvider('dataTest')] public function testParsingDesiredTypeDescription(string $description, Type $expectedType): void { $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); @@ -83,9 +82,7 @@ public function testParsingDesiredTypeDescription(string $description, Type $exp $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 - */ + #[DataProvider('dataTest')] public function testDesiredTypeDescription(string $description, Type $expectedType): void { $this->assertSame($description, $expectedType->describe(VerbosityLevel::value())); diff --git a/tests/PHPStan/Reflection/AllowedSubTypesClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/AllowedSubTypesClassReflectionExtensionTest.php index 58890ef52e..5ca035e52c 100644 --- a/tests/PHPStan/Reflection/AllowedSubTypesClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/AllowedSubTypesClassReflectionExtensionTest.php @@ -3,19 +3,20 @@ namespace PHPStan\Reflection; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class AllowedSubTypesClassReflectionExtensionTest extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/allowed-sub-types.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/allowed-sub-types.php'); } /** - * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, diff --git a/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php index dfdb480fb8..2be55e8284 100644 --- a/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php @@ -8,11 +8,11 @@ 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; @@ -20,7 +20,7 @@ class AnnotationsMethodsClassReflectionExtensionTest extends PHPStanTestCase { - public function dataMethods(): array + public static function dataMethods(): array { $fooMethods = [ 'getInteger' => [ @@ -959,12 +959,12 @@ public function dataMethods(): array } /** - * @dataProvider dataMethods * @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); @@ -974,7 +974,7 @@ public function testMethods(string $className, array $methods): void $this->assertTrue($class->hasMethod($methodName), sprintf('Method %s() not found in class %s.', $methodName, $className)); $method = $class->getMethod($methodName, $scope); - $selectedParametersAcceptor = ParametersAcceptorSelector::selectSingle($method->getVariants()); + $selectedParametersAcceptor = $method->getOnlyVariant(); $this->assertSame( $expectedMethodData['class'], $method->getDeclaringClass()->getName(), @@ -1026,7 +1026,7 @@ public function testMethods(string $className, array $methods): void public function testOverridingNativeMethodsWithAnnotationsDoesNotBreakGetNativeMethod(): void { - $reflectionProvider = $this->createReflectionProvider(); + $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 d58eeb7217..9e75d926b3 100644 --- a/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php @@ -11,12 +11,13 @@ use PHPStan\Analyser\Scope; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; use function sprintf; class AnnotationsPropertiesClassReflectionExtensionTest extends PHPStanTestCase { - public function dataProperties(): array + public static function dataProperties(): array { return [ [ @@ -272,17 +273,19 @@ public function dataProperties(): array } /** - * @dataProvider dataProperties * @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), @@ -320,7 +323,7 @@ public function testProperties(string $className, array $properties): void public function testOverridingNativePropertiesWithAnnotationsDoesNotBreakGetNativeProperty(): void { - $reflectionProvider = $this->createReflectionProvider(); + $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 75fbfa4527..b48fa67162 100644 --- a/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php @@ -10,14 +10,20 @@ 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 PHPStanTestCase { - public function dataDeprecatedAnnotations(): array + public static function dataDeprecatedAnnotations(): array { return [ [ @@ -84,17 +90,19 @@ public function dataDeprecatedAnnotations(): array } /** - * @dataProvider dataDeprecatedAnnotations * @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()); @@ -122,7 +130,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()); @@ -130,7 +138,7 @@ 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()); @@ -139,18 +147,247 @@ public function testNonDeprecatedNativeFunctions(): void public function testDeprecatedMethodsFromInterface(): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $class = $reflectionProvider->getClass(DeprecatedBar::class); $this->assertTrue($class->getNativeMethod('superDeprecated')->isDeprecated()->yes()); } public function testNotDeprecatedChildMethods(): void { - $reflectionProvider = $this->createReflectionProvider(); + $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 fb61a5c90e..b24c82fba9 100644 --- a/tests/PHPStan/Reflection/Annotations/FinalAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/FinalAnnotationsTest.php @@ -4,14 +4,14 @@ use FinalAnnotations\FinalFoo; use FinalAnnotations\Foo; -use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class FinalAnnotationsTest extends PHPStanTestCase { - public function dataFinalAnnotations(): array + public static function dataFinalAnnotations(): array { return [ [ @@ -38,17 +38,19 @@ public function dataFinalAnnotations(): array } /** - * @dataProvider dataFinalAnnotations * @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()); @@ -58,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 b734fac05c..79017d46cb 100644 --- a/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php @@ -11,11 +11,12 @@ use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class InternalAnnotationsTest extends PHPStanTestCase { - public function dataInternalAnnotations(): array + public static function dataInternalAnnotations(): array { return [ [ @@ -110,17 +111,19 @@ public function dataInternalAnnotations(): array } /** - * @dataProvider dataInternalAnnotations * @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()); @@ -144,7 +147,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 7e3f56b299..b58322ca01 100644 --- a/tests/PHPStan/Reflection/Annotations/ThrowsAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/ThrowsAnnotationsTest.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; use RuntimeException; use ThrowsAnnotations\BarTrait; use ThrowsAnnotations\Foo; @@ -16,7 +17,7 @@ class ThrowsAnnotationsTest extends PHPStanTestCase { - public function dataThrowsAnnotations(): array + public static function dataThrowsAnnotations(): array { return [ [ @@ -68,12 +69,12 @@ public function dataThrowsAnnotations(): array } /** - * @dataProvider dataThrowsAnnotations * @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); @@ -88,7 +89,7 @@ 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()); 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/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 index 9310f617f1..b6ee413032 100644 --- a/tests/PHPStan/Reflection/AnonymousClassReflectionTest.php +++ b/tests/PHPStan/Reflection/AnonymousClassReflectionTest.php @@ -101,6 +101,55 @@ public function testReflection(): void ]), 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/OptimizedDirectorySourceLocatorTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php index c9726f3247..06c9407908 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php @@ -8,6 +8,7 @@ use PHPStan\BetterReflection\Reflector\DefaultReflector; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; use TestDirectorySourceLocator\AFoo; use TestDirectorySourceLocator\EmptyClass; use function array_map; @@ -17,7 +18,7 @@ class OptimizedDirectorySourceLocatorTest extends PHPStanTestCase { - public function dataClass(): iterable + public static function dataClass(): iterable { yield from [ [ @@ -70,9 +71,7 @@ public function dataClass(): iterable ]; } - /** - * @dataProvider dataClass - */ + #[DataProvider('dataClass')] public function testClass(string $className, string $expectedClassName, string $file): void { $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); @@ -84,7 +83,7 @@ public function testClass(string $className, string $expectedClassName, string $ $this->assertSame($file, basename($classReflection->getFileName())); } - public function dataFunctionExists(): array + public static function dataFunctionExists(): array { return [ [ @@ -130,9 +129,7 @@ public function dataFunctionExists(): array ]; } - /** - * @dataProvider dataFunctionExists - */ + #[DataProvider('dataFunctionExists')] public function testFunctionExists(string $functionName, string $expectedFunctionName, string $file): void { $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); @@ -144,7 +141,7 @@ public function testFunctionExists(string $functionName, string $expectedFunctio $this->assertSame($file, basename($functionReflection->getFileName())); } - public function dataConstant(): iterable + public static function dataConstant(): iterable { yield from [ [ @@ -202,9 +199,7 @@ public function dataConstant(): iterable ]; } - /** - * @dataProvider dataConstant - */ + #[DataProvider('dataConstant')] public function testConstant(string $constantName, ?string $expectedFile): void { $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); @@ -290,7 +285,7 @@ public function testLocateIdentifiersByType(): void ], $actualConstants); } - public function dataFunctionDoesNotExist(): array + public static function dataFunctionDoesNotExist(): array { return [ ['doFoo'], @@ -298,9 +293,7 @@ public function dataFunctionDoesNotExist(): array ]; } - /** - * @dataProvider dataFunctionDoesNotExist - */ + #[DataProvider('dataFunctionDoesNotExist')] public function testFunctionDoesNotExist(string $functionName): void { $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); @@ -313,10 +306,6 @@ public function testFunctionDoesNotExist(string $functionName): void public function testBug5525(): void { - if (PHP_VERSION_ID < 70300) { - self::markTestSkipped('This test needs at least PHP 7.3 because of different PCRE engine'); - } - $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); $locator = $factory->createByFiles([__DIR__ . '/data/bug-5525.php']); $reflector = new DefaultReflector($locator); diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php index 80b588643f..f8c0875f69 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php @@ -10,6 +10,7 @@ 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; @@ -18,7 +19,7 @@ class OptimizedSingleFileSourceLocatorTest extends PHPStanTestCase { - public function dataClass(): iterable + public static function dataClass(): iterable { yield from [ [ @@ -54,7 +55,7 @@ public function dataClass(): iterable ]; } - public function dataForIdenifiersByType(): iterable + public static function dataForIdenifiersByType(): iterable { yield from [ 'classes wrapped in conditions' => [ @@ -127,9 +128,7 @@ public function dataForIdenifiersByType(): iterable ]; } - /** - * @dataProvider dataClass - */ + #[DataProvider('dataClass')] public function testClass(string $className, string $expectedClassName, string $file): void { $factory = self::getContainer()->getByType(OptimizedSingleFileSourceLocatorFactory::class); @@ -139,7 +138,7 @@ public function testClass(string $className, string $expectedClassName, string $ $this->assertSame($expectedClassName, $classReflection->getName()); } - public function dataFunction(): array + public static function dataFunction(): array { return [ [ @@ -165,9 +164,7 @@ public function dataFunction(): array ]; } - /** - * @dataProvider dataFunction - */ + #[DataProvider('dataFunction')] public function testFunction(string $functionName, string $expectedFunctionName, string $file): void { $factory = self::getContainer()->getByType(OptimizedSingleFileSourceLocatorFactory::class); @@ -177,7 +174,7 @@ public function testFunction(string $functionName, string $expectedFunctionName, $this->assertSame($expectedFunctionName, $functionReflection->getName()); } - public function dataConst(): array + public static function dataConst(): array { return [ [ @@ -203,9 +200,7 @@ public function dataConst(): array ]; } - /** - * @dataProvider dataConst - */ + #[DataProvider('dataConst')] public function testConst(string $constantName, string $valueTypeDescription): void { $factory = self::getContainer()->getByType(OptimizedSingleFileSourceLocatorFactory::class); @@ -222,16 +217,14 @@ public function testConst(string $constantName, string $valueTypeDescription): v $this->assertSame($valueTypeDescription, $valueType->describe(VerbosityLevel::precise())); } - public function dataConstUnknown(): array + public static function dataConstUnknown(): array { return [ ['TEST_VARIABLE'], ]; } - /** - * @dataProvider dataConstUnknown - */ + #[DataProvider('dataConstUnknown')] public function testConstUnknown(string $constantName): void { $factory = self::getContainer()->getByType(OptimizedSingleFileSourceLocatorFactory::class); @@ -242,9 +235,9 @@ public function testConstUnknown(string $constantName): void } /** - * @dataProvider dataForIdenifiersByType * @param class-string[] $expectedIdentifiers */ + #[DataProvider('dataForIdenifiersByType')] public function testLocateIdentifiersByType( IdentifierType $identifierType, array $expectedIdentifiers, 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 fa00afc63f..63c527ebd0 100644 --- a/tests/PHPStan/Reflection/ClassReflectionTest.php +++ b/tests/PHPStan/Reflection/ClassReflectionTest.php @@ -30,17 +30,18 @@ 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; -use const PHP_VERSION_ID; class ClassReflectionTest extends PHPStanTestCase { - public function dataHasTraitUse(): array + public static function dataHasTraitUse(): array { return [ [Foo::class, true], @@ -50,17 +51,17 @@ public function dataHasTraitUse(): array } /** - * @dataProvider dataHasTraitUse * @param class-string $className */ + #[DataProvider('dataHasTraitUse')] public function testHasTraitUse(string $className, bool $has): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $classReflection = $reflectionProvider->getClass($className); $this->assertSame($has, $classReflection->hasTraitUse(FooTrait::class)); } - public function dataClassHierarchyDistances(): array + public static function dataClassHierarchyDistances(): array { return [ [ @@ -93,16 +94,16 @@ public function dataClassHierarchyDistances(): array } /** - * @dataProvider dataClassHierarchyDistances * @param class-string $class * @param int[] $expectedDistances */ + #[DataProvider('dataClassHierarchyDistances')] public function testClassHierarchyDistances( string $class, array $expectedDistances, ): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $classReflection = $reflectionProvider->getClass($class); $this->assertSame( $expectedDistances, @@ -112,16 +113,16 @@ public function testClassHierarchyDistances( public function testVariadicTraitMethod(): void { - $reflectionProvider = $this->createReflectionProvider(); + $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(); + $reflectionProvider = self::createReflectionProvider(); $reflection = $reflectionProvider->getClass(C::class); $this->assertSame('GenericInheritance\\C', $reflection->getDisplayName()); @@ -140,12 +141,12 @@ public function testGenericInheritance(): void public function testIsGenericWithStubPhpDoc(): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $reflection = $reflectionProvider->getClass(ReflectionClass::class); $this->assertTrue($reflection->isGeneric()); } - public function dataIsAttributeClass(): array + public static function dataIsAttributeClass(): array { return [ [ @@ -169,12 +170,11 @@ public function dataIsAttributeClass(): array ]; } - /** - * @dataProvider dataIsAttributeClass - */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataIsAttributeClass')] public function testIsAttributeClass(string $className, bool $expected, int $expectedFlags = Attribute::TARGET_ALL): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $reflection = $reflectionProvider->getClass($className); $this->assertSame($expected, $reflection->isAttributeClass()); if (!$expected) { @@ -185,20 +185,20 @@ 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 */ + #[DataProvider('dataNestedRecursiveTraits')] public function testGetTraits(string $className, array $expected, bool $recursive): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $this->assertSame( array_map( @@ -209,7 +209,7 @@ public function testGetTraits(string $className, array $expected, bool $recursiv ); } - public function dataNestedRecursiveTraits(): array + public static function dataNestedRecursiveTraits(): array { return [ [ @@ -284,27 +284,23 @@ public function dataNestedRecursiveTraits(): array ]; } + #[RequiresPhp('>= 8.1')] public function testEnumIsFinal(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - - $reflectionProvider = $this->createReflectionProvider(); + $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 { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $enum = $reflectionProvider->getClass('PHPStan\Fixture\TestEnum'); $this->assertInstanceOf(IntegerType::class, $enum->getBackedEnumType()); } @@ -313,7 +309,7 @@ public function testIs(): void { $className = static::class; - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $classReflection = $reflectionProvider->getClass($className); $this->assertTrue($classReflection->is($className)); diff --git a/tests/PHPStan/Reflection/Constant/RuntimeConstantReflectionTest.php b/tests/PHPStan/Reflection/Constant/RuntimeConstantReflectionTest.php index 77d575716e..925cb4350d 100644 --- a/tests/PHPStan/Reflection/Constant/RuntimeConstantReflectionTest.php +++ b/tests/PHPStan/Reflection/Constant/RuntimeConstantReflectionTest.php @@ -5,12 +5,13 @@ use PhpParser\Node\Name; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPUnit\Framework\Attributes\DataProvider; use const PHP_VERSION_ID; class RuntimeConstantReflectionTest extends PHPStanTestCase { - public function dataDeprecatedConstants(): iterable + public static function dataDeprecatedConstants(): iterable { yield [ new Name('\FILTER_SANITIZE_STRING'), @@ -41,14 +42,12 @@ public function dataDeprecatedConstants(): iterable ]; } - /** - * @dataProvider dataDeprecatedConstants - */ + #[DataProvider('dataDeprecatedConstants')] public function testDeprecatedConstants(Name $constName, TrinaryLogic $isDeprecated, ?string $deprecationMessage): void { require_once __DIR__ . '/data/deprecated-constant.php'; - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $this->assertTrue($reflectionProvider->hasConstant($constName, null)); $this->assertSame($isDeprecated->describe(), $reflectionProvider->getConstant($constName, null)->isDeprecated()->describe()); diff --git a/tests/PHPStan/Reflection/Deprecation/DeprecationProviderTest.php b/tests/PHPStan/Reflection/Deprecation/DeprecationProviderTest.php new file mode 100644 index 0000000000..e88a5b66f6 --- /dev/null +++ b/tests/PHPStan/Reflection/Deprecation/DeprecationProviderTest.php @@ -0,0 +1,228 @@ += 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->getProperty('foo', $scopeForNotDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($notDeprecatedClass->getProperty('foo', $scopeForNotDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedClass->getProperty('foo', $scopeForDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($attributeDeprecatedClass->getProperty('foo', $scopeForDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedClass->getProperty('foo', $scopeForPhpDocDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($phpDocDeprecatedClass->getProperty('foo', $scopeForPhpDocDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedClassWithMessages->getProperty('foo', $scopeForPhpDocDeprecatedClassWithMessages)->isDeprecated()->yes()); + self::assertSame('phpdoc', $phpDocDeprecatedClassWithMessages->getProperty('foo', $scopeForPhpDocDeprecatedClassWithMessages)->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedClassWithMessages->getProperty('foo', $scopeForAttributeDeprecatedClassWithMessages)->isDeprecated()->yes()); + self::assertSame('attribute', $attributeDeprecatedClassWithMessages->getProperty('foo', $scopeForAttributeDeprecatedClassWithMessages)->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClass->getProperty('foo', $scopeForDoubleDeprecatedClass)->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedClass->getProperty('foo', $scopeForDoubleDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClassOnlyPhpDocMessage->getProperty('foo', $scopeForDoubleDeprecatedClassOnlyNativeMessage)->isDeprecated()->yes()); + self::assertNull($doubleDeprecatedClassOnlyPhpDocMessage->getProperty('foo', $scopeForDoubleDeprecatedClassOnlyNativeMessage)->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClassOnlyAttributeMessage->getProperty('foo', $scopeForDoubleDeprecatedClassOnlyCustomMessage)->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedClassOnlyAttributeMessage->getProperty('foo', $scopeForDoubleDeprecatedClassOnlyCustomMessage)->getDeprecatedDescription()); + + // methods + self::assertFalse($notDeprecatedClass->getMethod('foo', $scopeForNotDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($notDeprecatedClass->getMethod('foo', $scopeForNotDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedClass->getMethod('foo', $scopeForDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($attributeDeprecatedClass->getMethod('foo', $scopeForDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedClass->getMethod('foo', $scopeForPhpDocDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($phpDocDeprecatedClass->getMethod('foo', $scopeForPhpDocDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedClassWithMessages->getMethod('foo', $scopeForPhpDocDeprecatedClassWithMessages)->isDeprecated()->yes()); + self::assertSame('phpdoc', $phpDocDeprecatedClassWithMessages->getMethod('foo', $scopeForPhpDocDeprecatedClassWithMessages)->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedClassWithMessages->getMethod('foo', $scopeForAttributeDeprecatedClassWithMessages)->isDeprecated()->yes()); + self::assertSame('attribute', $attributeDeprecatedClassWithMessages->getMethod('foo', $scopeForAttributeDeprecatedClassWithMessages)->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClass->getMethod('foo', $scopeForDoubleDeprecatedClass)->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedClass->getMethod('foo', $scopeForDoubleDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClassOnlyPhpDocMessage->getMethod('foo', $scopeForDoubleDeprecatedClassOnlyNativeMessage)->isDeprecated()->yes()); + self::assertNull($doubleDeprecatedClassOnlyPhpDocMessage->getMethod('foo', $scopeForDoubleDeprecatedClassOnlyNativeMessage)->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClassOnlyAttributeMessage->getMethod('foo', $scopeForDoubleDeprecatedClassOnlyCustomMessage)->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedClassOnlyAttributeMessage->getMethod('foo', $scopeForDoubleDeprecatedClassOnlyCustomMessage)->getDeprecatedDescription()); + + // functions + self::assertFalse($notDeprecatedFunction->isDeprecated()->yes()); + self::assertNull($notDeprecatedFunction->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedFunction->isDeprecated()->yes()); + self::assertNull($phpDocDeprecatedFunction->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedFunctionWithMessage->isDeprecated()->yes()); + self::assertSame('phpdoc', $phpDocDeprecatedFunctionWithMessage->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedFunction->isDeprecated()->yes()); + self::assertNull($attributeDeprecatedFunction->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedFunctionWithMessage->isDeprecated()->yes()); + self::assertSame('attribute', $attributeDeprecatedFunctionWithMessage->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedFunction->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedFunction->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedFunctionOnlyPhpDocMessage->isDeprecated()->yes()); + self::assertNull($doubleDeprecatedFunctionOnlyPhpDocMessage->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedFunctionOnlyAttributeMessage->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedFunctionOnlyAttributeMessage->getDeprecatedDescription()); + } + + #[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..dd0ac38c86 --- /dev/null +++ b/tests/PHPStan/Reflection/Deprecation/data/CustomDeprecationExtension.php @@ -0,0 +1,82 @@ += 8.0 + +declare(strict_types = 1); + +namespace PHPStan\Tests; + +use CustomDeprecations\CustomDeprecated; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClassConstant; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnum; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumBackedCase; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumUnitCase; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty; +use PHPStan\BetterReflection\Reflection\ReflectionConstant; +use PHPStan\Reflection\Deprecation\ClassConstantDeprecationExtension; +use PHPStan\Reflection\Deprecation\ClassDeprecationExtension; +use PHPStan\Reflection\Deprecation\ConstantDeprecationExtension; +use PHPStan\Reflection\Deprecation\Deprecation; +use PHPStan\Reflection\Deprecation\EnumCaseDeprecationExtension; +use PHPStan\Reflection\Deprecation\FunctionDeprecationExtension; +use PHPStan\Reflection\Deprecation\MethodDeprecationExtension; +use PHPStan\Reflection\Deprecation\PropertyDeprecationExtension; + +class CustomDeprecationExtension implements + ConstantDeprecationExtension, + ClassDeprecationExtension, + ClassConstantDeprecationExtension, + MethodDeprecationExtension, + PropertyDeprecationExtension, + FunctionDeprecationExtension, + EnumCaseDeprecationExtension +{ + + public function getClassDeprecation(ReflectionClass|ReflectionEnum $reflection): ?Deprecation + { + return $this->buildDeprecation($reflection); + } + + public function getConstantDeprecation(ReflectionConstant $reflection): ?Deprecation + { + return $this->buildDeprecation($reflection); + } + + public function getFunctionDeprecation(ReflectionFunction $reflection): ?Deprecation + { + return $this->buildDeprecation($reflection); + } + + public function getMethodDeprecation(ReflectionMethod $reflection): ?Deprecation + { + return $this->buildDeprecation($reflection); + } + + public function getPropertyDeprecation(ReflectionProperty $reflection): ?Deprecation + { + return $this->buildDeprecation($reflection); + } + + public function getClassConstantDeprecation(ReflectionClassConstant $reflection): ?Deprecation + { + return $this->buildDeprecation($reflection); + } + + public function getEnumCaseDeprecation(ReflectionEnumBackedCase|ReflectionEnumUnitCase $reflection): ?Deprecation + { + return $this->buildDeprecation($reflection); + } + + private function buildDeprecation($reflection): ?Deprecation + { + foreach ($reflection->getAttributes(CustomDeprecated::class) as $attribute) { + $description = $attribute->getArguments()[0] ?? $attribute->getArguments()['description'] ?? null; + return $description === null + ? Deprecation::create() + : Deprecation::createWithDescription($description); + } + + return null; + } +} diff --git a/tests/PHPStan/Reflection/Deprecation/data/deprecation-provider.neon b/tests/PHPStan/Reflection/Deprecation/data/deprecation-provider.neon new file mode 100644 index 0000000000..6b79cfe3f2 --- /dev/null +++ b/tests/PHPStan/Reflection/Deprecation/data/deprecation-provider.neon @@ -0,0 +1,11 @@ +services: + - + class: PHPStan\Tests\CustomDeprecationExtension + tags: + - phpstan.propertyDeprecationExtension + - phpstan.methodDeprecationExtension + - phpstan.classConstantDeprecationExtension + - phpstan.classDeprecationExtension + - phpstan.functionDeprecationExtension + - phpstan.constantDeprecationExtension + - phpstan.enumCaseDeprecationExtension diff --git a/tests/PHPStan/Reflection/Deprecation/data/deprecations-enums.php b/tests/PHPStan/Reflection/Deprecation/data/deprecations-enums.php new file mode 100644 index 0000000000..44710f9e9f --- /dev/null +++ b/tests/PHPStan/Reflection/Deprecation/data/deprecations-enums.php @@ -0,0 +1,21 @@ += 8.1 + +namespace CustomDeprecations; + +#[CustomDeprecated] +enum MyDeprecatedEnum: string +{ + #[CustomDeprecated('custom')] + case CustomDeprecated = '1'; + + /** + * @deprecated phpdoc + */ + case PhpDocDeprecated = '2'; + + #[\Deprecated('native')] + case NativeDeprecated = '3'; + + case NotDeprecated = '4'; + +} diff --git a/tests/PHPStan/Reflection/Deprecation/data/deprecations.php b/tests/PHPStan/Reflection/Deprecation/data/deprecations.php new file mode 100644 index 0000000000..9df05b1b41 --- /dev/null +++ b/tests/PHPStan/Reflection/Deprecation/data/deprecations.php @@ -0,0 +1,151 @@ += 8.1 + +namespace CustomDeprecations; + +class NotDeprecatedClass +{ + const FOO = 'foo'; + + private $foo; + + public function foo() {} + +} + + +/** @deprecated */ +class PhpDocDeprecatedClass +{ + + /** @deprecated */ + const FOO = 'foo'; + + /** @deprecated */ + private $foo; + + /** @deprecated */ + public function foo() {} + +} +/** @deprecated phpdoc */ +class PhpDocDeprecatedClassWithMessage +{ + + /** @deprecated phpdoc */ + const FOO = 'foo'; + + /** @deprecated phpdoc */ + private $foo; + + /** @deprecated phpdoc */ + public function foo() {} + +} + +#[CustomDeprecated] +class AttributeDeprecatedClass { + #[CustomDeprecated] + public const FOO = 'foo'; + + #[CustomDeprecated] + private $foo; + + #[CustomDeprecated] + public function foo() {} +} + +#[CustomDeprecated('attribute')] +class AttributeDeprecatedClassWithMessage { + #[CustomDeprecated('attribute')] + const FOO = 'foo'; + + #[CustomDeprecated('attribute')] + private $foo; + + #[CustomDeprecated(description: 'attribute')] + public function foo() {} +} + +/** @deprecated phpdoc */ +#[CustomDeprecated('attribute')] +class DoubleDeprecatedClass +{ + + /** @deprecated phpdoc */ + #[CustomDeprecated('attribute')] + const FOO = 'foo'; + + /** @deprecated phpdoc */ + #[CustomDeprecated('attribute')] + private $foo; + + /** @deprecated phpdoc */ + #[CustomDeprecated('attribute')] + public function foo() {} + +} + +/** @deprecated */ +#[CustomDeprecated('attribute')] +class DoubleDeprecatedClassOnlyAttributeMessage +{ + + /** @deprecated */ + #[CustomDeprecated('attribute')] + const FOO = 'foo'; + + /** @deprecated */ + #[CustomDeprecated('attribute')] + private $foo; + + /** @deprecated */ + #[CustomDeprecated('attribute')] + public function foo() {} + +} + +/** @deprecated phpdoc */ +#[CustomDeprecated()] +class DoubleDeprecatedClassOnlyPhpDocMessage +{ + + /** @deprecated phpdoc */ + #[CustomDeprecated()] + const FOO = 'foo'; + + /** @deprecated phpdoc */ + #[CustomDeprecated()] + private $foo; + + /** @deprecated phpdoc */ + #[CustomDeprecated()] + public function foo() {} + +} + + +function notDeprecatedFunction() {} + +/** @deprecated */ +function phpDocDeprecatedFunction() {} + +/** @deprecated phpdoc */ +function phpDocDeprecatedFunctionWithMessage() {} + +#[CustomDeprecated] +function attributeDeprecatedFunction() {} + +#[CustomDeprecated('attribute')] +function attributeDeprecatedFunctionWithMessage() {} + +/** @deprecated phpdoc */ +#[CustomDeprecated('attribute')] +function doubleDeprecatedFunction() {} + +/** @deprecated */ +#[CustomDeprecated('attribute')] +function doubleDeprecatedFunctionOnlyAttributeMessage() {} + +/** @deprecated phpdoc */ +#[CustomDeprecated()] +function doubleDeprecatedFunctionOnlyPhpDocMessage() {} diff --git a/tests/PHPStan/Reflection/FunctionReflectionTest.php b/tests/PHPStan/Reflection/FunctionReflectionTest.php index 0b57c66a84..12dd5ee438 100644 --- a/tests/PHPStan/Reflection/FunctionReflectionTest.php +++ b/tests/PHPStan/Reflection/FunctionReflectionTest.php @@ -6,12 +6,13 @@ use PHPStan\Analyser\Scope; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPUnit\Framework\Attributes\DataProvider; use const PHP_VERSION_ID; class FunctionReflectionTest extends PHPStanTestCase { - public function dataPhpdocFunctions(): iterable + public static function dataPhpdocFunctions(): iterable { yield [ 'FunctionReflectionDocTest\\myFunction', @@ -36,21 +37,20 @@ public function dataPhpdocFunctions(): iterable } /** - * @dataProvider dataPhpdocFunctions - * * @param non-empty-string $functionName */ + #[DataProvider('dataPhpdocFunctions')] public function testFunctionHasPhpdoc(string $functionName, ?string $expectedDoc): void { require_once __DIR__ . '/data/function-with-phpdoc.php'; - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $functionReflection = $reflectionProvider->getFunction(new Node\Name($functionName), null); $this->assertSame($expectedDoc, $functionReflection->getDocComment()); } - public function dataPhpdocMethods(): iterable + public static function dataPhpdocMethods(): iterable { yield [ 'FunctionReflectionDocTest\\ClassWithPhpdoc', @@ -114,24 +114,24 @@ public function dataPhpdocMethods(): iterable ]; } - /** - * @dataProvider dataPhpdocMethods - */ + #[DataProvider('dataPhpdocMethods')] public function testMethodHasPhpdoc(string $className, string $methodName, ?string $expectedDocComment): 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); $classReflection = $reflectionProvider->getClass($className); $methodReflection = $classReflection->getMethod($methodName, $scope); $this->assertSame($expectedDocComment, $methodReflection->getDocComment()); } - public function dataFunctionReturnsByReference(): iterable + public static function dataFunctionReturnsByReference(): iterable { yield ['\\implode', TrinaryLogic::createNo()]; @@ -140,20 +140,20 @@ public function dataFunctionReturnsByReference(): iterable } /** - * @dataProvider dataFunctionReturnsByReference * @param non-empty-string $functionName */ + #[DataProvider('dataFunctionReturnsByReference')] public function testFunctionReturnsByReference(string $functionName, TrinaryLogic $expectedReturnsByRef): void { require_once __DIR__ . '/data/returns-by-reference.php'; - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $functionReflection = $reflectionProvider->getFunction(new Node\Name($functionName), null); $this->assertSame($expectedReturnsByRef, $functionReflection->returnsByReference()); } - public function dataMethodReturnsByReference(): iterable + public static function dataMethodReturnsByReference(): iterable { yield ['ReturnsByReference\\X', 'foo', TrinaryLogic::createNo()]; yield ['ReturnsByReference\\X', 'refFoo', TrinaryLogic::createYes()]; @@ -175,17 +175,17 @@ public function dataMethodReturnsByReference(): iterable yield ['ReturnsByReference\\E', 'cases', TrinaryLogic::createNo()]; } - /** - * @dataProvider dataMethodReturnsByReference - */ + #[DataProvider('dataMethodReturnsByReference')] public function testMethodReturnsByReference(string $className, string $methodName, TrinaryLogic $expectedReturnsByRef): 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); $classReflection = $reflectionProvider->getClass($className); $methodReflection = $classReflection->getMethod($methodName, $scope); diff --git a/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php b/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php index 94306bc0ec..ea627a029f 100644 --- a/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php +++ b/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php @@ -17,6 +17,7 @@ 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; @@ -27,7 +28,7 @@ class GenericParametersAcceptorResolverTest extends PHPStanTestCase /** * @return array */ - public function dataResolve(): array + public static function dataResolve(): array { $templateType = static fn ($name, ?Type $type = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), @@ -422,9 +423,9 @@ public function dataResolve(): array } /** - * @dataProvider dataResolve * @param Type[] $argTypes */ + #[DataProvider('dataResolve')] public function testResolve(array $argTypes, ParametersAcceptor $parametersAcceptor, ParametersAcceptor $expectedResult): void { self::getContainer(); // to initialize bleeding edge diff --git a/tests/PHPStan/Reflection/InitializerExprTypeResolverTest.php b/tests/PHPStan/Reflection/InitializerExprTypeResolverTest.php index 9288f62e8d..6c3ba79cfb 100644 --- a/tests/PHPStan/Reflection/InitializerExprTypeResolverTest.php +++ b/tests/PHPStan/Reflection/InitializerExprTypeResolverTest.php @@ -10,11 +10,12 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; +use PHPUnit\Framework\Attributes\DataProvider; class InitializerExprTypeResolverTest extends PHPStanTestCase { - public function dataExplicitNever(): iterable + public static function dataExplicitNever(): iterable { yield [ new LNumber(1), @@ -100,11 +101,11 @@ static function (Expr $expr): Type { } /** - * @dataProvider dataExplicitNever * * @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); diff --git a/tests/PHPStan/Reflection/MixedTypeTest.php b/tests/PHPStan/Reflection/MixedTypeTest.php index f6c511df33..0d30fc39b8 100644 --- a/tests/PHPStan/Reflection/MixedTypeTest.php +++ b/tests/PHPStan/Reflection/MixedTypeTest.php @@ -6,20 +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 { - $reflectionProvider = $this->createReflectionProvider(); + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $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()); @@ -29,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 2b2b96c00f..8c375776f8 100644 --- a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php +++ b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php @@ -8,6 +8,7 @@ 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; @@ -15,9 +16,11 @@ 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; @@ -28,15 +31,21 @@ 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 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 [ @@ -68,7 +77,27 @@ public function dataSelectFromTypes(): Generator ], $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 [ [ @@ -79,7 +108,27 @@ public function dataSelectFromTypes(): Generator ], $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 [ [ @@ -144,35 +193,6 @@ public function dataSelectFromTypes(): Generator ), ]; - $absVariants = $reflectionProvider->getFunction(new Name('abs'), null)->getVariants(); - yield [ - [ - new FloatType(), - new FloatType(), - ], - $absVariants, - false, - ParametersAcceptorSelector::combineAcceptors($absVariants), - ]; - yield [ - [ - new FloatType(), - new IntegerType(), - new StringType(), - ], - $absVariants, - false, - ParametersAcceptorSelector::combineAcceptors($absVariants), - ]; - yield [ - [ - new StringType(), - ], - $absVariants, - false, - $absVariants[2], - ]; - $strtokVariants = $reflectionProvider->getFunction(new Name('strtok'), null)->getVariants(); yield [ [], @@ -452,10 +472,10 @@ public function dataSelectFromTypes(): Generator } /** - * @dataProvider dataSelectFromTypes * @param Type[] $types * @param ParametersAcceptor[] $variants */ + #[DataProvider('dataSelectFromTypes')] public function testSelectFromTypes( array $types, array $variants, diff --git a/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php index dabd3948e0..9c2858aaea 100644 --- a/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php @@ -13,7 +13,7 @@ class UniversalObjectCratesClassReflectionExtensionTest extends PHPStanTestCase public function testNonexistentClass(): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $extension = new UniversalObjectCratesClassReflectionExtension( $reflectionProvider, ['NonexistentClass', 'stdClass'], @@ -26,7 +26,7 @@ public function testDifferentGetSetType(): void { require_once __DIR__ . '/data/universal-object-crates.php'; - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $extension = new UniversalObjectCratesClassReflectionExtension( $reflectionProvider, ['UniversalObjectCreates\DifferentGetSetTypes'], @@ -52,7 +52,7 @@ public function testAnnotationOverrides(): void require_once __DIR__ . '/data/universal-object-crates-annotations.php'; $className = 'UniversalObjectCratesAnnotations\Model'; - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $extension = new UniversalObjectCratesClassReflectionExtension( $reflectionProvider, [$className], diff --git a/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php b/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php index d7b8ee4599..c9f990d1cd 100644 --- a/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php +++ b/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection; use JetBrains\PHPStormStub\PhpStormStubsMap; +use Override; use PhpParser\Node; use PhpParser\Node\Name; use PhpParser\NodeTraverser; @@ -12,6 +13,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; use ReflectionClass; use Symfony\Component\Finder\Finder; use Throwable; @@ -28,6 +30,7 @@ use function get_defined_functions; use function getenv; use function implode; +use function is_bool; use function mkdir; use function sort; use function strpos; @@ -62,7 +65,7 @@ public static function data(): iterable } } - /** @dataProvider data */ + #[DataProvider('data')] public function test(string $input, string $expectedOutput): void { $output = self::generateSymbolDescription($input); @@ -341,7 +344,7 @@ private static function generateFunctionMethodBaseDescription($reflection): stri $result .= 'Is deprecated: ' . $reflection->isDeprecated()->describe() . "\n"; } - if (! $reflection->isFinal()->no()) { + if ($reflection instanceof MethodReflection && ! $reflection->isFinal()->no()) { $result .= 'Is final: ' . $reflection->isFinal()->describe() . "\n"; } @@ -349,6 +352,14 @@ private static function generateFunctionMethodBaseDescription($reflection): stri $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"; } @@ -364,7 +375,7 @@ private static function generateFunctionMethodBaseDescription($reflection): stri return $result; } - /** @param ParametersAcceptorWithPhpDocs[] $variants */ + /** @param ExtendedParametersAcceptor[] $variants */ private static function generateVariantsDescription(string $name, array $variants, bool $isNamedArguments): string { $variantCount = count($variants); @@ -482,7 +493,7 @@ public static function dumpInputSymbols(): void { $symbols = self::scrapeInputSymbols(); $symbolsFile = self::getPhpSymbolsFile(); - @mkdir(dirname($symbolsFile), 0777, true); + @mkdir(dirname($symbolsFile), recursive: true); $result = file_put_contents($symbolsFile, implode("\n", $symbols)); if ($result !== false) { @@ -625,6 +636,7 @@ private static function scrapeSymbolsFromStubs(array $stubFiles): array private Node\Stmt\ClassLike $classLike; + #[Override] public function enterNode(Node $node) { if ($node instanceof Node\Stmt\ClassLike && $node->namespacedName !== null) { @@ -647,6 +659,7 @@ public function enterNode(Node $node) return null; } + #[Override] public function leaveNode(Node $node) { if ($node instanceof Node\Stmt\ClassLike) { diff --git a/tests/PHPStan/Reflection/ReflectionProviderTest.php b/tests/PHPStan/Reflection/ReflectionProviderTest.php index 02dfd6869f..2e538e5f32 100644 --- a/tests/PHPStan/Reflection/ReflectionProviderTest.php +++ b/tests/PHPStan/Reflection/ReflectionProviderTest.php @@ -9,12 +9,14 @@ 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', @@ -50,13 +52,12 @@ public function dataFunctionThrowType(): iterable } /** - * @dataProvider dataFunctionThrowType - * * @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) { @@ -70,7 +71,7 @@ public function testFunctionThrowType(string $functionName, ?Type $expectedThrow ); } - public function dataFunctionDeprecated(): iterable + public static function dataFunctionDeprecated(): iterable { if (PHP_VERSION_ID < 80000) { yield 'create_function' => [ @@ -97,24 +98,23 @@ public function dataFunctionDeprecated(): iterable } /** - * @dataProvider dataFunctionDeprecated - * * @param non-empty-string $functionName */ + #[DataProvider('dataFunctionDeprecated')] public function testFunctionDeprecated(string $functionName, bool $isDeprecated): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $function = $reflectionProvider->getFunction(new Name($functionName), null); $this->assertEquals(TrinaryLogic::createFromBoolean($isDeprecated), $function->isDeprecated()); } - public function dataMethodThrowType(): array + public static function dataMethodThrowType(): array { return [ [ DateTime::class, '__construct', - new ObjectType('DateMalformedStringException'), + PHP_VERSION_ID >= 80300 ? new ObjectType('DateMalformedStringException') : new ObjectType('Exception'), ], [ DateTime::class, @@ -124,12 +124,10 @@ public function dataMethodThrowType(): array ]; } - /** - * @dataProvider dataMethodThrowType - */ + #[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(); @@ -144,15 +142,12 @@ public function testMethodThrowType(string $className, string $methodName, ?Type ); } + #[RequiresPhp('>= 8.3')] public function testNativeClassConstantTypeInEvaledClass(): void { - if (PHP_VERSION_ID < 80300) { - $this->markTestSkipped('Test requires PHP 8.3.'); - } - eval('namespace NativeClassConstantInEvaledClass; class Foo { public const int FOO = 1; }'); - $reflectionProvider = $this->createReflectionProvider(); + $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/Php8SignatureMapProviderTest.php b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php index 88a1b9d9c7..8c422e1500 100644 --- a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php @@ -10,8 +10,11 @@ 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; @@ -21,6 +24,7 @@ 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; @@ -30,6 +34,7 @@ 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; @@ -38,7 +43,7 @@ class Php8SignatureMapProviderTest extends PHPStanTestCase { - public function dataFunctions(): array + public static function dataFunctions(): array { return [ [ @@ -59,7 +64,7 @@ public function dataFunctions(): array 'variadic' => false, ], ], - new UnionType([ + new BenevolentUnionType([ new ObjectType('CurlHandle'), new ConstantBooleanType(false), ]), @@ -97,14 +102,14 @@ public function dataFunctions(): array new ConstantStringType('errors'), ], [ IntegerRangeType::fromInterval(0, null), - new ArrayType(new IntegerType(), new StringType()), + new IntersectionType([new ArrayType(IntegerRangeType::fromInterval(0, null), new StringType()), new AccessoryArrayListType()]), IntegerRangeType::fromInterval(0, null), - new ArrayType(new IntegerType(), new StringType()), + 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, ], @@ -128,9 +133,9 @@ public function dataFunctions(): array } /** - * @dataProvider dataFunctions * @param mixed[] $parameters */ + #[DataProvider('dataFunctions')] public function testFunctions( string $functionName, array $parameters, @@ -161,10 +166,11 @@ private function createProvider(): Php8SignatureMapProvider 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 [ [ @@ -203,7 +209,7 @@ public function dataMethods(): array 'variadic' => false, ], ], - new UnionType([ + new BenevolentUnionType([ new ObjectType('Closure'), new NullType(), ]), @@ -257,9 +263,9 @@ public function dataMethods(): array } /** - * @dataProvider dataMethods * @param mixed[] $parameters */ + #[DataProvider('dataMethods')] public function testMethods( string $className, string $methodName, @@ -302,15 +308,13 @@ private function assertSignature( $this->assertSame($expectedVariadic, $actualSignature->isVariadic()); } - public function dataParseAll(): array + public static function dataParseAll(): array { $map = new Php8StubsMap(PHP_VERSION_ID); return array_map(static fn (string $file): array => [__DIR__ . '/../../../../vendor/phpstan/php-8-stubs/' . $file], array_merge($map->classes, $map->functions)); } - /** - * @dataProvider dataParseAll - */ + #[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 00ed1a4159..98b47e6cb0 100644 --- a/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php @@ -28,6 +28,7 @@ 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; @@ -39,9 +40,9 @@ 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'], @@ -424,9 +425,9 @@ public function dataGetFunctions(): array } /** - * @dataProvider dataGetFunctions * @param mixed[] $map */ + #[DataProvider('dataGetFunctions')] public function testGetFunctions( array $map, ?string $className, @@ -482,7 +483,7 @@ public function testGetFunctions( ); } - public function dataParseAll(): array + public static function dataParseAll(): array { return [ [70400], @@ -490,9 +491,7 @@ public function dataParseAll(): array ]; } - /** - * @dataProvider dataParseAll - */ + #[DataProvider('dataParseAll')] public function testParseAll(int $phpVersionId): void { $parser = self::getContainer()->getByType(SignatureMapParser::class); diff --git a/tests/PHPStan/Reflection/UnionTypesTest.php b/tests/PHPStan/Reflection/UnionTypesTest.php index d8977504d2..8b72103e33 100644 --- a/tests/PHPStan/Reflection/UnionTypesTest.php +++ b/tests/PHPStan/Reflection/UnionTypesTest.php @@ -15,14 +15,14 @@ public function testUnionTypes(): void { 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())); @@ -32,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/anonymous-classes.php b/tests/PHPStan/Reflection/data/anonymous-classes.php index 9336316ff9..4024445aec 100644 --- a/tests/PHPStan/Reflection/data/anonymous-classes.php +++ b/tests/PHPStan/Reflection/data/anonymous-classes.php @@ -11,3 +11,23 @@ public function __construct(object $object) { } }; + +class A {} + +new class extends A {}; new class extends A {}; + +new class (new class extends A {}) extends A { + public function __construct(object $object) + { + } +}; + +interface U {} + +interface V {} + +new class implements U {}; + +new class implements U, V {}; + +new class implements V, U {}; diff --git a/tests/PHPStan/Reflection/data/attribute-reflection-enum.php b/tests/PHPStan/Reflection/data/attribute-reflection-enum.php new file mode 100644 index 0000000000..fd02812679 --- /dev/null +++ b/tests/PHPStan/Reflection/data/attribute-reflection-enum.php @@ -0,0 +1,11 @@ += 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/Rules/Api/ApiClassConstFetchRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassConstFetchRuleTest.php index f5a0b797ab..b139a1ef2a 100644 --- a/tests/PHPStan/Rules/Api/ApiClassConstFetchRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiClassConstFetchRuleTest.php @@ -14,7 +14,7 @@ class ApiClassConstFetchRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ApiClassConstFetchRule(new ApiRuleHelper(), $this->createReflectionProvider()); + return new ApiClassConstFetchRule(new ApiRuleHelper(), self::createReflectionProvider()); } public function testRuleInPhpStan(): void diff --git a/tests/PHPStan/Rules/Api/ApiClassExtendsRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassExtendsRuleTest.php index 26651b4e97..f62d2c2225 100644 --- a/tests/PHPStan/Rules/Api/ApiClassExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiClassExtendsRuleTest.php @@ -14,7 +14,7 @@ class ApiClassExtendsRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ApiClassExtendsRule(new ApiRuleHelper(), $this->createReflectionProvider()); + return new ApiClassExtendsRule(new ApiRuleHelper(), self::createReflectionProvider()); } public function testRuleInPhpStan(): void diff --git a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php index a6a68a9b5e..2b0022ce56 100644 --- a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php @@ -14,7 +14,7 @@ class ApiClassImplementsRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ApiClassImplementsRule(new ApiRuleHelper(), $this->createReflectionProvider()); + return new ApiClassImplementsRule(new ApiRuleHelper(), self::createReflectionProvider()); } public function testRuleInPhpStan(): void @@ -32,32 +32,32 @@ public function testRuleOutOfPhpStan(): void $this->analyse([__DIR__ . '/data/class-implements-out-of-phpstan.php'], [ [ 'Implementing PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 19, + 20, $tip, ], [ 'Implementing PHPStan\Type\Type is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 53, + 54, $tip, ], [ 'Implementing PHPStan\Reflection\ReflectionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 338, + 333, $tip, ], [ 'Implementing PHPStan\Analyser\Scope is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 343, + 338, $tip, ], [ 'Implementing PHPStan\Reflection\FunctionReflection is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 348, + 343, $tip, ], [ 'Implementing PHPStan\Reflection\ExtendedMethodReflection is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 352, + 347, $tip, ], ]); diff --git a/tests/PHPStan/Rules/Api/ApiInstanceofRuleTest.php b/tests/PHPStan/Rules/Api/ApiInstanceofRuleTest.php index a22b102c63..edba6b82e1 100644 --- a/tests/PHPStan/Rules/Api/ApiInstanceofRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiInstanceofRuleTest.php @@ -14,7 +14,7 @@ class ApiInstanceofRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ApiInstanceofRule(new ApiRuleHelper(), $this->createReflectionProvider()); + return new ApiInstanceofRule(new ApiRuleHelper(), self::createReflectionProvider()); } public function testRuleInPhpStan(): void diff --git a/tests/PHPStan/Rules/Api/ApiInstanceofTypeRuleTest.php b/tests/PHPStan/Rules/Api/ApiInstanceofTypeRuleTest.php index 6be9f18d4b..08ceeff3fd 100644 --- a/tests/PHPStan/Rules/Api/ApiInstanceofTypeRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiInstanceofTypeRuleTest.php @@ -13,7 +13,7 @@ class ApiInstanceofTypeRuleTest extends RuleTestCase public function getRule(): Rule { - return new ApiInstanceofTypeRule($this->createReflectionProvider(), true, true); + return new ApiInstanceofTypeRule(self::createReflectionProvider()); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Api/ApiInstantiationRuleTest.php b/tests/PHPStan/Rules/Api/ApiInstantiationRuleTest.php index 0d96c5a664..e2e968aadd 100644 --- a/tests/PHPStan/Rules/Api/ApiInstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiInstantiationRuleTest.php @@ -16,7 +16,7 @@ protected function getRule(): Rule { return new ApiInstantiationRule( new ApiRuleHelper(), - $this->createReflectionProvider(), + self::createReflectionProvider(), ); } diff --git a/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php b/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php index 770c59da3a..ee499665b8 100644 --- a/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php @@ -14,7 +14,7 @@ class ApiInterfaceExtendsRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ApiInterfaceExtendsRule(new ApiRuleHelper(), $this->createReflectionProvider()); + return new ApiInterfaceExtendsRule(new ApiRuleHelper(), self::createReflectionProvider()); } public function testRuleInPhpStan(): void diff --git a/tests/PHPStan/Rules/Api/ApiRuleHelperTest.php b/tests/PHPStan/Rules/Api/ApiRuleHelperTest.php index 93d8428b77..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,9 +134,7 @@ public function dataIsPhpStanCode(): array ]; } - /** - * @dataProvider dataIsPhpStanCode - */ + #[DataProvider('dataIsPhpStanCode')] public function testIsPhpStanCode( ?string $scopeNamespace, string $scopeFile, diff --git a/tests/PHPStan/Rules/Api/ApiStaticCallRuleTest.php b/tests/PHPStan/Rules/Api/ApiStaticCallRuleTest.php index 8331674e4b..26da521670 100644 --- a/tests/PHPStan/Rules/Api/ApiStaticCallRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiStaticCallRuleTest.php @@ -14,7 +14,7 @@ class ApiStaticCallRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ApiStaticCallRule(new ApiRuleHelper(), $this->createReflectionProvider()); + return new ApiStaticCallRule(new ApiRuleHelper(), self::createReflectionProvider()); } public function testRuleInPhpStan(): void diff --git a/tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php b/tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php index da2dbbeefe..d15bbf5aa1 100644 --- a/tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php @@ -14,7 +14,7 @@ class ApiTraitUseRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ApiTraitUseRule(new ApiRuleHelper(), $this->createReflectionProvider()); + return new ApiTraitUseRule(new ApiRuleHelper(), self::createReflectionProvider()); } public function testRuleInPhpStan(): void diff --git a/tests/PHPStan/Rules/Api/GetTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Api/GetTemplateTypeRuleTest.php index c65e8a6fd4..fab3cc8528 100644 --- a/tests/PHPStan/Rules/Api/GetTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Api/GetTemplateTypeRuleTest.php @@ -13,7 +13,7 @@ class GetTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new GetTemplateTypeRule($this->createReflectionProvider()); + return new GetTemplateTypeRule(self::createReflectionProvider()); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Api/NodeConnectingVisitorAttributesRuleTest.php b/tests/PHPStan/Rules/Api/NodeConnectingVisitorAttributesRuleTest.php index 2e5388d2b1..b811039ec7 100644 --- a/tests/PHPStan/Rules/Api/NodeConnectingVisitorAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Api/NodeConnectingVisitorAttributesRuleTest.php @@ -13,7 +13,7 @@ class NodeConnectingVisitorAttributesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new NodeConnectingVisitorAttributesRule(self::getContainer()); + return new NodeConnectingVisitorAttributesRule(); } public function testRule(): void @@ -21,7 +21,12 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/node-connecting-visitor.php'], [ [ 'Node attribute \'parent\' is no longer available.', - 18, + 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/NodeConnectingVisitorAttributesRuleWithVisitorRegisteredTest.php b/tests/PHPStan/Rules/Api/NodeConnectingVisitorAttributesRuleWithVisitorRegisteredTest.php deleted file mode 100644 index 569d7747eb..0000000000 --- a/tests/PHPStan/Rules/Api/NodeConnectingVisitorAttributesRuleWithVisitorRegisteredTest.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ -class NodeConnectingVisitorAttributesRuleWithVisitorRegisteredTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new NodeConnectingVisitorAttributesRule(self::getContainer()); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/node-connecting-visitor.php'], []); - } - - public static function getAdditionalConfigFiles(): array - { - return array_merge(parent::getAdditionalConfigFiles(), [ - __DIR__ . '/nodeConnectingVisitorCompatibility.neon', - ]); - } - -} 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 4c50e26ccc..aaa875710d 100644 --- a/tests/PHPStan/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRuleTest.php +++ b/tests/PHPStan/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Api; use Nette\Utils\Json; +use Override; use PHPStan\File\FileWriter; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -19,6 +20,7 @@ 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 index f043e062fc..a1b0203897 100644 --- a/tests/PHPStan/Rules/Api/RuntimeReflectionFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Api/RuntimeReflectionFunctionRuleTest.php @@ -13,7 +13,7 @@ class RuntimeReflectionFunctionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new RuntimeReflectionFunctionRule($this->createReflectionProvider()); + return new RuntimeReflectionFunctionRule(self::createReflectionProvider()); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationRuleTest.php b/tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationRuleTest.php index ff9861230f..0ec03856c5 100644 --- a/tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationRuleTest.php @@ -14,7 +14,7 @@ class RuntimeReflectionInstantiationRuleTest extends RuleTestCase protected function getRule(): Rule { - return new RuntimeReflectionInstantiationRule($this->createReflectionProvider()); + return new RuntimeReflectionInstantiationRule(self::createReflectionProvider()); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php b/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php index c7b42d7e6e..6ded7325fb 100644 --- a/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php +++ b/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\FunctionReflection; @@ -127,7 +128,7 @@ public function hasConstant(string $constantName): \PHPStan\TrinaryLogic // TODO: Implement hasConstant() method. } - public function getConstant(string $constantName): \PHPStan\Reflection\ConstantReflection + public function getConstant(string $constantName): \PHPStan\Reflection\ClassConstantReflection { // TODO: Implement getConstant() method. } @@ -247,12 +248,12 @@ public function toArrayKey(): \PHPStan\Type\Type // TODO: Implement toArrayKey() method. } - public function isSmallerThan(Type $otherType): \PHPStan\TrinaryLogic + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): \PHPStan\TrinaryLogic { // TODO: Implement isSmallerThan() method. } - public function isSmallerThanOrEqual(Type $otherType): \PHPStan\TrinaryLogic + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): \PHPStan\TrinaryLogic { // TODO: Implement isSmallerThanOrEqual() method. } @@ -277,22 +278,22 @@ public function isLiteralString(): \PHPStan\TrinaryLogic // TODO: Implement isLiteralString() method. } - public function getSmallerType(): \PHPStan\Type\Type + public function getSmallerType(PhpVersion $phpVersion): \PHPStan\Type\Type { // TODO: Implement getSmallerType() method. } - public function getSmallerOrEqualType(): \PHPStan\Type\Type + public function getSmallerOrEqualType(PhpVersion $phpVersion): \PHPStan\Type\Type { // TODO: Implement getSmallerOrEqualType() method. } - public function getGreaterType(): \PHPStan\Type\Type + public function getGreaterType(PhpVersion $phpVersion): \PHPStan\Type\Type { // TODO: Implement getGreaterType() method. } - public function getGreaterOrEqualType(): \PHPStan\Type\Type + public function getGreaterOrEqualType(PhpVersion $phpVersion): \PHPStan\Type\Type { // TODO: Implement getGreaterOrEqualType() method. } @@ -327,12 +328,6 @@ public function tryRemove(Type $typeToRemove): ?Type // TODO: Implement tryRemove() method. } - public static function __set_state(array $properties): \PHPStan\Type\Type - { - // TODO: Implement __set_state() method. - } - - } abstract class Dolor implements ReflectionProvider diff --git a/tests/PHPStan/Rules/Api/data/node-connecting-visitor.php b/tests/PHPStan/Rules/Api/data/node-connecting-visitor.php index e61c7c3a4d..93f18f28a9 100644 --- a/tests/PHPStan/Rules/Api/data/node-connecting-visitor.php +++ b/tests/PHPStan/Rules/Api/data/node-connecting-visitor.php @@ -8,6 +8,10 @@ class MyRule implements Rule { + + /** @var 'parent'|'myCustomAttribute' */ + public string $attrName; + public function getNodeType(): string { return Node::class; @@ -17,6 +21,7 @@ public function processNode(Node $node, Scope $scope): array { $parent = $node->getAttribute("parent"); $custom = $node->getAttribute("myCustomAttribute"); + $parent = $node->getAttribute($this->attrName); return []; } 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/Api/nodeConnectingVisitorCompatibility.neon b/tests/PHPStan/Rules/Api/nodeConnectingVisitorCompatibility.neon deleted file mode 100644 index efb020baea..0000000000 --- a/tests/PHPStan/Rules/Api/nodeConnectingVisitorCompatibility.neon +++ /dev/null @@ -1,3 +0,0 @@ -conditionalTags: - PhpParser\NodeVisitor\NodeConnectingVisitor: - phpstan.parser.richParserNodeVisitor: true diff --git a/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php b/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php deleted file mode 100644 index ea09ce0cb4..0000000000 --- a/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php +++ /dev/null @@ -1,65 +0,0 @@ - - */ -class AppendedArrayItemTypeRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new AppendedArrayItemTypeRule( - new PropertyReflectionFinder(), - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false), - ); - } - - public function testAppendedArrayItemType(): void - { - $this->analyse( - [__DIR__ . '/data/appended-array-item.php'], - [ - [ - 'Array (array) does not accept string.', - 18, - ], - [ - 'Array (array) does not accept array{1, 2, 3}.', - 20, - ], - [ - 'Array (array) does not accept array{\'AppendedArrayItem\\\\Foo\', \'classMethod\'}.', - 23, - ], - [ - 'Array (array) does not accept array{\'Foo\', \'Hello world\'}.', - 25, - ], - [ - 'Array (array) does not accept string.', - 27, - ], - [ - 'Array (array) does not accept string.', - 32, - ], - [ - 'Array (array) does not accept Closure(): 1.', - 45, - ], - [ - 'Array (array) does not accept AppendedArrayItem\Baz.', - 79, - ], - ], - ); - } - -} diff --git a/tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php b/tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php deleted file mode 100644 index d74a1ddd8e..0000000000 --- a/tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php +++ /dev/null @@ -1,71 +0,0 @@ - - */ -class AppendedArrayKeyTypeRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new AppendedArrayKeyTypeRule( - new PropertyReflectionFinder(), - true, - ); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/appended-array-key.php'], [ - [ - 'Array (array) does not accept key int|string.', - 28, - ], - [ - 'Array (array) does not accept key string.', - 30, - ], - [ - 'Array (array) does not accept key int.', - 31, - ], - [ - 'Array (array) does not accept key int|string.', - 33, - ], - [ - 'Array (array) does not accept key 0.', - 38, - ], - [ - 'Array (array) does not accept key 1.', - 46, - ], - [ - 'Array (array<1|2|3, string>) does not accept key int.', - 80, - ], - [ - 'Array (array<1|2|3, string>) does not accept key 4.', - 85, - ], - ]); - } - - public function testBug5372Two(): void - { - $this->analyse([__DIR__ . '/data/bug-5372_2.php'], []); - } - - public function testBug5447(): void - { - $this->analyse([__DIR__ . '/data/bug-5447.php'], []); - } - -} diff --git a/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php b/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php index a536ce6cf5..b105abcd36 100644 --- a/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php @@ -5,7 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -13,15 +13,13 @@ class ArrayDestructuringRuleTest extends RuleTestCase { - private bool $bleedingEdge = false; - protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false); + $ruleLevelHelper = new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true); return new ArrayDestructuringRule( $ruleLevelHelper, - new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, $this->bleedingEdge, false, false), + new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, false, false), ); } @@ -51,12 +49,9 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.0')] public function testRuleWithNullsafeVariant(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/array-destructuring-nullsafe.php'], [ [ 'Cannot use array destructuring on array|null.', diff --git a/tests/PHPStan/Rules/Arrays/ArrayUnpackingRuleTest.php b/tests/PHPStan/Rules/Arrays/ArrayUnpackingRuleTest.php index b733df51cd..826d3e33f1 100644 --- a/tests/PHPStan/Rules/Arrays/ArrayUnpackingRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/ArrayUnpackingRuleTest.php @@ -6,7 +6,8 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -22,16 +23,13 @@ protected function getRule(): Rule { return new ArrayUnpackingRule( self::getContainer()->getByType(PhpVersion::class), - new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnions, false, false, true, $this->checkBenevolentUnions), + new RuleLevelHelper(self::createReflectionProvider(), true, false, $this->checkUnions, false, false, $this->checkBenevolentUnions, true), ); } + #[RequiresPhp('< 8.1')] public function testRule(): void { - if (PHP_VERSION_ID >= 80100) { - $this->markTestSkipped('Test requires PHP version <= 8.0'); - } - $this->checkUnions = true; $this->checkBenevolentUnions = true; $this->analyse([__DIR__ . '/data/array-unpacking.php'], [ @@ -52,11 +50,11 @@ public function testRule(): void 29, ], [ - 'Array unpacking cannot be used on an array with potential string keys: array', + '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', + 'Array unpacking cannot be used on an array with potential string keys: array', 52, ], [ @@ -70,12 +68,9 @@ public function testRule(): void ]); } + #[RequiresPhp('< 8.1')] public function testRuleDoNotCheckBenevolentUnion(): void { - if (PHP_VERSION_ID >= 80100) { - $this->markTestSkipped('Test requires PHP version <= 8.0'); - } - $this->checkUnions = true; $this->analyse([__DIR__ . '/data/array-unpacking.php'], [ [ @@ -86,14 +81,6 @@ public function testRuleDoNotCheckBenevolentUnion(): void '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', - 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, @@ -105,12 +92,9 @@ public function testRuleDoNotCheckBenevolentUnion(): void ]); } + #[RequiresPhp('< 8.1')] public function testRuleDoNotCheckUnions(): void { - if (PHP_VERSION_ID >= 80100) { - $this->markTestSkipped('Test requires PHP version <= 8.0'); - } - $this->checkUnions = false; $this->analyse([__DIR__ . '/data/array-unpacking.php'], [ [ @@ -124,7 +108,7 @@ public function testRuleDoNotCheckUnions(): void ]); } - public function dataRuleOnPHP81(): array + public static function dataRuleOnPHP81(): array { return [ [true], @@ -132,15 +116,10 @@ public function dataRuleOnPHP81(): array ]; } - /** - * @dataProvider dataRuleOnPHP81 - */ + #[RequiresPhp('>= 8.1')] + #[DataProvider('dataRuleOnPHP81')] public function testRuleOnPHP81(bool $checkUnions): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1+'); - } - $this->checkUnions = $checkUnions; $this->analyse([__DIR__ . '/data/array-unpacking.php'], []); } diff --git a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php index a99a20557c..d6c1445dfa 100644 --- a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php @@ -40,4 +40,9 @@ public function testBug8292(): void $this->analyse([__DIR__ . '/data/bug-8292.php'], []); } + public function testBug13248(): void + { + $this->analyse([__DIR__ . '/data/bug-13248.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php b/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php index 87b5a12e5f..03a01b9250 100644 --- a/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php @@ -61,7 +61,53 @@ public function testDuplicateKeys(): void '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 70c671135a..0d42bca9e9 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php @@ -5,7 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -15,7 +15,7 @@ class InvalidKeyInArrayDimFetchRuleTest extends RuleTestCase protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false); + $ruleLevelHelper = new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true); return new InvalidKeyInArrayDimFetchRule($ruleLevelHelper, true); } @@ -61,12 +61,9 @@ public function testInvalidKey(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug6315(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-6315.php'], [ [ 'Invalid array key type Bug6315\FooEnum::A.', diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php index 7a40122d1c..ab486eeacf 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -63,12 +63,9 @@ public function testInvalidKeyShortArray(): void ]); } + #[RequiresPhp('>= 8.1')] public function testInvalidKeyEnum(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/invalid-key-array-item-enum.php'], [ [ 'Invalid array key type InvalidKeyArrayItemEnum\FooEnum::A.', diff --git a/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php b/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php index dbb64b29ca..e00b70e84e 100644 --- a/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php @@ -5,9 +5,10 @@ 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; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -21,7 +22,7 @@ class IterableInForeachRuleTest extends RuleTestCase protected function getRule(): Rule { - return new IterableInForeachRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false)); + return new IterableInForeachRule(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true)); } public function testCheckWithMaybes(): void @@ -32,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, ], [ @@ -58,12 +59,9 @@ public function testBug5744(): void ]); } + #[RequiresPhp('>= 8.0')] public function testRuleWithNullsafeVariant(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/foreach-iterable-nullsafe.php'], [ [ @@ -84,7 +82,7 @@ public function testBug4335(): void $this->analyse([__DIR__ . '/data/bug-4335.php'], []); } - public function dataMixed(): array + public static function dataMixed(): array { $explicitOnlyErrors = [ [ @@ -130,9 +128,10 @@ public function dataMixed(): array } /** - * @dataProvider dataMixed * @param list $errors */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataMixed')] public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void { $this->checkExplicitMixed = $checkExplicitMixed; @@ -140,4 +139,10 @@ public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, ar $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 deacff9371..a481085088 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -5,6 +5,8 @@ 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; /** @@ -17,166 +19,23 @@ class NonexistentOffsetInArrayDimFetchRuleTest extends RuleTestCase private bool $checkImplicitMixed = false; - private bool $bleedingEdge = false; - private bool $reportPossiblyNonexistentGeneralArrayOffset = false; private bool $reportPossiblyNonexistentConstantArrayOffset = false; protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false); + $ruleLevelHelper = new RuleLevelHelper(self::createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true); return new NonexistentOffsetInArrayDimFetchRule( $ruleLevelHelper, - new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, $this->bleedingEdge, $this->reportPossiblyNonexistentGeneralArrayOffset, $this->reportPossiblyNonexistentConstantArrayOffset), + new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, $this->reportPossiblyNonexistentGeneralArrayOffset, $this->reportPossiblyNonexistentConstantArrayOffset), true, ); } public function testRule(): void { - $this->analyse([__DIR__ . '/data/nonexistent-offset.php'], [ - [ - 'Offset \'b\' does not exist on array{a: stdClass, 0: 2}.', - 17, - ], - [ - 'Offset 1 does not exist on array{a: stdClass, 0: 2}.', - 18, - ], - [ - 'Offset \'a\' does not exist on array{b: 1}.', - 55, - ], - [ - 'Access to offset \'bar\' on an unknown class NonexistentOffset\Bar.', - 101, - 'Learn more at https://phpstan.org/user-guide/discovering-symbols', - ], - [ - 'Access to an offset on an unknown class NonexistentOffset\Bar.', - 102, - 'Learn more at https://phpstan.org/user-guide/discovering-symbols', - ], - [ - 'Offset 0 does not exist on array.', - 111, - ], - [ - 'Offset \'0\' does not exist on array.', - 112, - ], - [ - 'Offset int does not exist on array.', - 114, - ], - [ - 'Offset \'test\' does not exist on null.', - 126, - ], - [ - 'Cannot access offset 42 on int.', - 142, - ], - [ - 'Cannot access offset 42 on float.', - 143, - ], - [ - 'Cannot access offset 42 on bool.', - 144, - ], - [ - 'Cannot access offset 42 on resource.', - 145, - ], - [ - 'Offset \'c\' does 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}.', - 190, - ], - [ - 'Offset int does not exist on array{}|array{1: 1, 2: 2}|array{3: 3, 4: 4}.', - 193, - ], - [ - 'Offset \'b\' does not exist on array{a: \'blabla\'}.', - 225, - ], - [ - 'Offset \'b\' does not exist on array{a: \'blabla\'}.', - 228, - ], - [ - 'Cannot access offset \'a\' on Closure(): void.', - 253, - ], - [ - 'Cannot access offset \'a\' on array{a: 1, b: 1}|(Closure(): void).', - 258, - ], - [ - 'Offset null does not exist on array.', - 310, - ], - [ - 'Offset int does not exist on array.', - 312, - ], - [ - 'Offset \'baz\' does not exist on array{bar: 1, baz?: 2}.', - 344, - ], - [ - 'Offset \'foo\' does not exist on ArrayAccess.', - 411, - ], - [ - 'Cannot access offset \'foo\' on stdClass.', - 423, - ], - [ - 'Cannot access offset \'foo\' on true.', - 426, - ], - [ - 'Cannot access offset \'foo\' on false.', - 429, - ], - [ - 'Cannot access offset \'foo\' on resource.', - 433, - ], - [ - 'Cannot access offset \'foo\' on 42.', - 436, - ], - [ - 'Cannot access offset \'foo\' on 4.141.', - 439, - ], - [ - 'Cannot access offset \'foo\' on array|int.', - 443, - ], - [ - 'Offset \'feature_pretty…\' does 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, - ], - ]); - } - - public function testRuleBleedingEdge(): void - { - $this->bleedingEdge = true; $this->analyse([__DIR__ . '/data/nonexistent-offset.php'], [ [ 'Offset \'b\' does not exist on array{a: stdClass, 0: 2}.', @@ -305,7 +164,7 @@ public function testRuleBleedingEdge(): void 443, ], [ - 'Offset \'feature_pretty…\' 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}.', + '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, ], [ @@ -327,11 +186,11 @@ public function testStrings(): void 13, ], [ - 'Offset \'foo\' does not exist on array|string.', + 'Offset \'foo\' might not exist on array|string.', 24, ], [ - 'Offset 12.34 does not exist on array|string.', + 'Offset 12.34 might not exist on array|string.', 28, ], ]); @@ -484,12 +343,9 @@ public function testBug5744(): void ]); } + #[RequiresPhp('>= 8.0')] public function testRuleWithNullsafeVariant(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/nonexistent-offset-nullsafe.php'], [ [ 'Offset 1 does not exist on array{a: int}.', @@ -518,12 +374,9 @@ public function testBug6379(): void $this->analyse([__DIR__ . '/data/bug-6379.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug4885(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/bug-4885.php'], []); } @@ -531,7 +384,7 @@ public function testBug7000(): void { $this->analyse([__DIR__ . '/data/bug-7000.php'], [ [ - "Offset 'require'|'require-dev' does not exist on array{require?: array, require-dev?: array}.", + "Offset 'require'|'require-dev' might not exist on array{require?: array, require-dev?: array}.", 16, ], ]); @@ -627,12 +480,9 @@ public function testBug7469(): void $this->analyse([__DIR__ . '/data/bug-7469.php'], $expected); } + #[RequiresPhp('>= 8.1')] public function testBug7763(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-7763.php'], []); } @@ -687,19 +537,14 @@ public function testBug8068(): void public function testBug6243(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/bug-6243.php'], []); } public function testBug8356(): void { - $this->bleedingEdge = true; $this->analyse([__DIR__ . '/data/bug-8356.php'], [ [ - "Offset 'x' might not exist on array|string.", + "Offset 'x' might not exist on array|string.", 7, ], ]); @@ -760,6 +605,7 @@ public function testBug10926(): void ]); } + #[RequiresPhp('>= 8.0')] public function testMixed(): void { $this->checkExplicitMixed = true; @@ -814,7 +660,7 @@ public function testNonExistentParentOffsetAccessLegal(): void ]); } - public function dataReportPossiblyNonexistentArrayOffset(): iterable + public static function dataReportPossiblyNonexistentArrayOffset(): iterable { yield [false, false, []]; yield [false, true, [ @@ -842,9 +688,9 @@ public function dataReportPossiblyNonexistentArrayOffset(): iterable } /** - * @dataProvider dataReportPossiblyNonexistentArrayOffset * @param list $errors */ + #[DataProvider('dataReportPossiblyNonexistentArrayOffset')] public function testReportPossiblyNonexistentArrayOffset(bool $reportPossiblyNonexistentGeneralArrayOffset, bool $reportPossiblyNonexistentConstantArrayOffset, array $errors): void { $this->reportPossiblyNonexistentGeneralArrayOffset = $reportPossiblyNonexistentGeneralArrayOffset; @@ -864,4 +710,220 @@ public function testBug10997(): void ]); } + 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 testBug11602(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-11602.php'], []); + } + + public function testBug12593(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-12593.php'], []); + } + + public function testBug3747(): void + { + $this->analyse([__DIR__ . '/data/bug-3747.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php index 2daf7b7ed4..1c32d81285 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php @@ -5,7 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,7 +17,7 @@ class OffsetAccessAssignOpRuleTest extends RuleTestCase protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnions, false, false, true, false); + $ruleLevelHelper = new RuleLevelHelper(self::createReflectionProvider(), true, false, $this->checkUnions, false, false, false, true); return new OffsetAccessAssignOpRule($ruleLevelHelper); } @@ -38,12 +38,9 @@ public function testRuleWithoutUnions(): void $this->analyse([__DIR__ . '/data/offset-access-assignop.php'], []); } + #[RequiresPhp('>= 8.0')] public function testRuleWithNullsafeVariant(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkUnions = true; $this->analyse([__DIR__ . '/data/offset-access-assignop-nullsafe.php'], []); } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php index bf704ac725..1c78cb3f23 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php @@ -5,7 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,7 +17,7 @@ class OffsetAccessAssignmentRuleTest extends RuleTestCase protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnionTypes, false, false, true, false); + $ruleLevelHelper = new RuleLevelHelper(self::createReflectionProvider(), true, false, $this->checkUnionTypes, false, false, false, true); return new OffsetAccessAssignmentRule($ruleLevelHelper); } @@ -129,12 +129,9 @@ public function testAssignNewOffsetToStubbedClass(): void $this->analyse([__DIR__ . '/data/new-offset-stub.php'], []); } + #[RequiresPhp('>= 8.0')] public function testRuleWithNullsafeVariant(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkUnionTypes = true; $this->analyse([__DIR__ . '/data/offset-access-assignment-nullsafe.php'], [ [ @@ -156,4 +153,43 @@ public function testBug8015(): void $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 0129923ae8..4c9b910bad 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php @@ -5,7 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -15,7 +15,7 @@ class OffsetAccessValueAssignmentRuleTest extends RuleTestCase protected function getRule(): Rule { - return new OffsetAccessValueAssignmentRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false)); + return new OffsetAccessValueAssignmentRule(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true)); } public function testRule(): void @@ -52,12 +52,9 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.0')] public function testRuleWithNullsafeVariant(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/offset-access-value-assignment-nullsafe.php'], [ [ 'ArrayAccess does not accept int|null.', @@ -66,12 +63,9 @@ public function testRuleWithNullsafeVariant(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug5655b(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/bug-5655b.php'], []); } diff --git a/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php b/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php index 32d4900686..ce675567f2 100644 --- a/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php @@ -5,9 +5,10 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; use function array_merge; use function usort; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -21,7 +22,7 @@ class UnpackIterableInArrayRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UnpackIterableInArrayRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false)); + return new UnpackIterableInArrayRule(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true)); } public function testRule(): void @@ -42,12 +43,9 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.0')] public function testRuleWithNullsafeVariant(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/unpack-iterable-nullsafe.php'], [ [ 'Only iterables can be unpacked, array|null given.', @@ -56,7 +54,7 @@ public function testRuleWithNullsafeVariant(): void ]); } - public function dataMixed(): array + public static function dataMixed(): array { $explicitOnlyErrors = [ [ @@ -102,9 +100,10 @@ public function dataMixed(): array } /** - * @dataProvider dataMixed * @param list $errors */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataMixed')] public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void { $this->checkExplicitMixed = $checkExplicitMixed; 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 index d7d1e64de5..ff2f652cac 100644 --- a/tests/PHPStan/Rules/Arrays/data/array-unpacking.php +++ b/tests/PHPStan/Rules/Arrays/data/array-unpacking.php @@ -1,4 +1,4 @@ -= 7.4 + $tags + * @param numeric-string $tagId + */ +function printTagName(array $tags, string $tagId): void +{ + // Adding the second `*` to either of the following lines makes the error disappear + + $tagsById = array_combine(array_column($tags, 'id'), $tags); + if (false !== $tagsById) { + echo $tagsById[$tagId]['tagName'] . PHP_EOL; + } +} + +printTagName( + [ + ['id' => '123', 'tagName' => 'abc'], + ['id' => '4.5', 'tagName' => 'def'], + ['id' => '6e78', 'tagName' => 'ghi'] + ], + '4.5' +); diff --git a/tests/PHPStan/Rules/Arrays/data/bug-11447.php b/tests/PHPStan/Rules/Arrays/data/bug-11447.php new file mode 100644 index 0000000000..f59f2bdd6a --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-11447.php @@ -0,0 +1,8 @@ + $range + */ +function doInt(int $i, $range): void +{ + $i[] = 1; + $range[] = 1; +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-11602.php b/tests/PHPStan/Rules/Arrays/data/bug-11602.php new file mode 100644 index 0000000000..4e1252e5b4 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-11602.php @@ -0,0 +1,23 @@ +arr); + if (!isset($this->arr['foo'])) { + $this->arr['foo'] = true; + assertType('array{foo: true}', $this->arr); + } + assertType('array{foo: bool}', $this->arr); + return $this->arr['foo']; // PHPStan realizes optional 'foo' is set + } +} + +class NonworkingExample +{ + /** @var array */ + private array $arr = []; + + public function sayHello(int $index): bool + { + assertType('array', $this->arr); + if (!isset($this->arr[$index]['foo'])) { + $this->arr[$index]['foo'] = true; + assertType('non-empty-array', $this->arr); + } + assertType('array', $this->arr); + return $this->arr[$index]['foo']; // PHPStan does not realize 'foo' is set + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-12122.php b/tests/PHPStan/Rules/Arrays/data/bug-12122.php new file mode 100644 index 0000000000..acd1816675 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-12122.php @@ -0,0 +1,8 @@ + */ + protected array $words = []; + + public function sayHello(string $word, int $count): void + { + $this->words[$word] ??= 0; + $this->words[$word] += $count; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-12406b.php b/tests/PHPStan/Rules/Arrays/data/bug-12406b.php new file mode 100644 index 0000000000..c0012503d0 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-12406b.php @@ -0,0 +1,35 @@ +]+>\\n + AuthorDate:[^\\n]+\\n + Commit:[^\\n]+\\n + CommitDate:[^\\n]+\\n\\n + (\s+(?:[^\n]+\n)+)\n + [ ](\\d+)[ ]files?[ ]changed,(?:[ ](\\d+)[ ]insertions?\\(\\+\\),?)?(?:[ ](\\d+)[ ]deletions?\\(-\\))? + ~mx', $s, $matches, PREG_SET_ORDER); + + for ($i = 0; $i < count($matches); $i++) { + $author = $matches[$i][1]; + $files = (int) $matches[$i][3]; + $insertions = (int) ($matches[$i][4] ?? 0); + $deletions = (int) ($matches[$i][5] ?? 0); + + $stats[$author]['commits'] = ($stats[$author]['commits'] ?? 0) + 1; + $stats[$author]['files'] = ($stats[$author]['files'] ?? 0) + $files; + $stats[$author]['insertions'] = ($stats[$author]['insertions'] ?? 0) + $insertions; + $stats[$author]['deletions'] = ($stats[$author]['deletions'] ?? 0) + $deletions; + $stats[$author]['diff'] = ($stats[$author]['diff'] ?? 0) + $insertions - $deletions; + } + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-12593.php b/tests/PHPStan/Rules/Arrays/data/bug-12593.php new file mode 100644 index 0000000000..b5ba2616cf --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-12593.php @@ -0,0 +1,35 @@ + $indexes + */ + protected function removeArguments(array $indexes): void + { + if (isset($_SERVER['argv']) && is_array($_SERVER['argv'])) { + foreach ($indexes as $index) { + if (isset($_SERVER['argv'][$index])) { + unset($_SERVER['argv'][$index]); + } + } + } + } +} + +class HelloWorld2 +{ + /** + * @param list $indexes + */ + protected function removeArguments(array $indexes): void + { + foreach ($indexes as $index) { + if (isset($_SERVER['argv']) && is_array($_SERVER['argv']) && isset($_SERVER['argv'][$index])) { + unset($_SERVER['argv'][$index]); + } + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-12605.php b/tests/PHPStan/Rules/Arrays/data/bug-12605.php new file mode 100644 index 0000000000..c5d31f966c --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-12605.php @@ -0,0 +1,37 @@ + + */ +function test(): array +{ + return []; +} + +function doFoo(): void { + $test = test(); + + if (isset($test[3])) { + echo $test[1]; + } + echo $test[1]; +} + +function doFooBar(): void { + $test = test(); + + if (isset($test[4])) { + echo $test[10]; + } +} + +function doBaz(): void { + $test = test(); + + if (array_key_exists(5, $test) && is_int($test[5])) { + echo $test[3]; + } +} + diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13013.php b/tests/PHPStan/Rules/Arrays/data/bug-13013.php new file mode 100644 index 0000000000..dd5180869c --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-13013.php @@ -0,0 +1,77 @@ +> + */ + 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-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-2313.php b/tests/PHPStan/Rules/Arrays/data/bug-2313.php new file mode 100644 index 0000000000..f79d9f0add --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-2313.php @@ -0,0 +1,22 @@ + array()); + + safe_inc($data['apples']['count']); + print_r($data); + + safe_inc($data['apples']['count']); + print_r($data); +}; diff --git a/tests/PHPStan/Rules/Arrays/data/bug-2634.php b/tests/PHPStan/Rules/Arrays/data/bug-2634.php new file mode 100644 index 0000000000..355ce896f6 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-2634.php @@ -0,0 +1,13 @@ + $x */ + private static array $x; + + public function y(): void { + + self::$x = []; + + $this->z(); + + echo self::$x['foo']; + + } + + private function z(): void { + self::$x['foo'] = 'bar'; + } + +} + +$x = new X(); +$x->y(); diff --git a/tests/PHPStan/Rules/Arrays/data/bug-6243.php b/tests/PHPStan/Rules/Arrays/data/bug-6243.php index 97c62f8512..1bf44f1400 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-6243.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-6243.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 + '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/duplicate-keys.php b/tests/PHPStan/Rules/Arrays/data/duplicate-keys.php index 02176773ac..8e7725d5b8 100644 --- a/tests/PHPStan/Rules/Arrays/data/duplicate-keys.php +++ b/tests/PHPStan/Rules/Arrays/data/duplicate-keys.php @@ -92,4 +92,95 @@ public function doWithoutKeys(int $int) ]; } + /** + * @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 @@ -= 7.4 +isInInterval((float) $changeInPercents)) { $key = $percentageInterval->getFormatted(); if (array_key_exists($key, $intervalResults)) { - assertType('array', $intervalResults); + assertType('array', $intervalResults); assertType('array{itemsCount: mixed, interval: mixed}', $intervalResults[$key]); $intervalResults[$key]['itemsCount'] += $itemsCount; - assertType('non-empty-array', $intervalResults); + assertType('non-empty-array', $intervalResults); assertType('array{itemsCount: (array|float|int), interval: mixed}', $intervalResults[$key]); } else { - assertType('array', $intervalResults); + assertType('array', $intervalResults); assertType('array{itemsCount: mixed, interval: mixed}', $intervalResults[$key]); $intervalResults[$key] = [ 'itemsCount' => $itemsCount, 'interval' => $percentageInterval, ]; - assertType('non-empty-array', $intervalResults); + assertType('non-empty-array', $intervalResults); assertType('array{itemsCount: mixed, interval: mixed}', $intervalResults[$key]); } } } } - assertType('array', $intervalResults); + assertType('array', $intervalResults); foreach ($intervalResults as $data) { echo $data['interval']; } 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 + @@ -16,7 +16,7 @@ class EchoRuleTest extends RuleTestCase protected function getRule(): Rule { return new EchoRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false), + new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true), ); } @@ -47,15 +47,16 @@ public function testEchoRule(): void 'Parameter #1 (\'string\'|array{\'string\'}) of echo cannot be converted to string.', 17, ], + [ + 'Parameter #1 (array{}) of echo cannot be converted to string.', + 29, + ], ]); } + #[RequiresPhp('>= 8.0')] public function testRuleWithNullsafeVariant(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/echo-nullsafe.php'], [ [ 'Parameter #1 (array|null) of echo cannot be converted to string.', diff --git a/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php b/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php index 5734b47928..cad88fb000 100644 --- a/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php +++ b/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php @@ -5,9 +5,10 @@ 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; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -21,8 +22,8 @@ class InvalidCastRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new InvalidCastRule($broker, new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, 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 @@ -60,12 +61,9 @@ public function testBug5162(): void $this->analyse([__DIR__ . '/data/bug-5162.php'], []); } + #[RequiresPhp('>= 8.0')] public function testRuleWithNullsafeVariant(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/invalid-cast-nullsafe.php'], [ [ 'Cannot cast stdClass|null to string.', @@ -88,7 +86,7 @@ public function testCastObjectToString(): void ]); } - public function dataMixed(): array + public static function dataMixed(): array { $explicitOnlyErrors = [ [ @@ -158,9 +156,10 @@ public function dataMixed(): array } /** - * @dataProvider dataMixed * @param list $errors */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataMixed')] public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void { $this->checkImplicitMixed = $checkImplicitMixed; diff --git a/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php b/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php index 3ba7d5ce61..b42c44cd09 100644 --- a/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php +++ b/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php @@ -7,7 +7,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -19,7 +19,7 @@ protected function getRule(): Rule { return new InvalidPartOfEncapsedStringRule( new ExprPrinter(new Printer()), - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false), + new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true), ); } @@ -33,12 +33,9 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.0')] public function testRuleWithNullsafeVariant(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/invalid-encapsed-part-nullsafe.php'], [ [ 'Part $bar?->obj (stdClass|null) of encapsed string cannot be cast to string.', diff --git a/tests/PHPStan/Rules/Cast/PrintRuleTest.php b/tests/PHPStan/Rules/Cast/PrintRuleTest.php index c7a52e8123..509ab75570 100644 --- a/tests/PHPStan/Rules/Cast/PrintRuleTest.php +++ b/tests/PHPStan/Rules/Cast/PrintRuleTest.php @@ -5,7 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -16,7 +16,7 @@ class PrintRuleTest extends RuleTestCase protected function getRule(): Rule { return new PrintRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false), + new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true), ); } @@ -54,12 +54,9 @@ public function testPrintRule(): void ]); } + #[RequiresPhp('>= 8.0')] public function testRuleWithNullsafeVariant(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/print-nullsafe.php'], [ [ 'Parameter array|null of print cannot be converted to string.', diff --git a/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php b/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php index ebab5c0aa6..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 @@ -19,7 +20,7 @@ protected function getRule(): Rule return new UnsetCastRule(new PhpVersion($this->phpVersion)); } - public function dataRule(): array + public static function dataRule(): array { return [ [ @@ -39,9 +40,9 @@ public function dataRule(): array } /** - * @dataProvider dataRule * @param list $errors */ + #[DataProvider('dataRule')] public function testRule(int $phpVersion, array $errors): void { $this->phpVersion = $phpVersion; 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/Classes/AllowedSubTypesRuleTest.php b/tests/PHPStan/Rules/Classes/AllowedSubTypesRuleTest.php index 403a35e6ff..21852d48fa 100644 --- a/tests/PHPStan/Rules/Classes/AllowedSubTypesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/AllowedSubTypesRuleTest.php @@ -26,6 +26,24 @@ public function testRule(): void ]); } + 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 [ diff --git a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index 6fa6252277..c85ac4f704 100644 --- a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -14,7 +13,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -22,33 +21,38 @@ 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, false, false, true, false), + new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, - true, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, false), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), true, ), ); } + #[RequiresPhp('>= 8.0')] public function testRule(): void { $this->analyse([__DIR__ . '/data/class-attributes.php'], [ @@ -115,12 +119,9 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1')] public function testRuleForEnums(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/enum-attributes.php'], [ [ 'Attribute class EnumAttributes\AttributeWithPropertyTarget does not have the class target.', @@ -133,6 +134,7 @@ public function testRuleForEnums(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug7171(): void { $this->analyse([__DIR__ . '/data/bug-7171.php'], [ @@ -143,9 +145,44 @@ public function testBug7171(): void ]); } + #[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 b132e3fe08..a2a1f5328b 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -23,25 +22,25 @@ 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, false, false, true, false), + 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(), ), true, ), diff --git a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index 42378de1f2..9d24119731 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -9,6 +9,8 @@ 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; /** @@ -21,15 +23,18 @@ class ClassConstantRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ClassConstantRule( $reflectionProvider, - new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), + 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, ); } @@ -55,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, @@ -187,7 +196,7 @@ public function testClassExists(): void ]); } - public function dataClassConstantOnExpression(): array + public static function dataClassConstantOnExpression(): array { return [ [ @@ -236,9 +245,9 @@ public function dataClassConstantOnExpression(): array } /** - * @dataProvider dataClassConstantOnExpression * @param list $errors */ + #[DataProvider('dataClassConstantOnExpression')] public function testClassConstantOnExpression(int $phpVersion, array $errors): void { $this->phpVersion = $phpVersion; @@ -280,12 +289,9 @@ public function testAttributes(): void ]); } + #[RequiresPhp('>= 8.0')] public function testRuleWithNullsafeVariant(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->phpVersion = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/class-constant-nullsafe.php'], []); } @@ -420,4 +426,122 @@ public function testPhpstanInternalClass(): void ]); } + #[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/DuplicateDeclarationRuleTest.php b/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php index e75c590756..c0f21a6053 100644 --- a/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -65,6 +66,7 @@ public function testDuplicatePromotedProperty(): void ]); } + #[RequiresPhp('>= 8.1')] public function testDuplicateEnumCase(): void { $this->analyse([__DIR__ . '/data/duplicate-enum-cases.php'], [ diff --git a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php index c1e85d214f..22bb10a00e 100644 --- a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php +++ b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,17 +17,15 @@ protected function getRule(): Rule return new EnumSanityRule(); } + #[RequiresPhp('>= 8.1')] public function testRule(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1'); - } - $expected = [ - [ + /*[ + // reported by AbstractMethodInNonAbstractClassRule 'Enum EnumSanity\EnumWithAbstractMethod contains abstract method foo().', 7, - ], + ],*/ [ 'Enum EnumSanity\EnumWithConstructorAndDestructor contains constructor.', 12, @@ -109,12 +107,9 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/enum-sanity.php'], $expected); } + #[RequiresPhp('>= 8.1')] public function testBug9402(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1'); - } - $this->analyse([__DIR__ . '/data/bug-9402.php'], [ [ 'Enum case Bug9402\Foo::Two value \'foo\' does not match the "int" type.', @@ -123,4 +118,27 @@ public function testBug9402(): void ]); } + #[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 fa3ca260c9..4ae78ffe28 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php @@ -7,7 +7,7 @@ use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,13 +17,16 @@ class ExistingClassInClassExtendsRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ExistingClassInClassExtendsRule( new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), $reflectionProvider, + true, ); } @@ -78,12 +81,9 @@ public function testFinalByTag(): void ]); } + #[RequiresPhp('>= 8.1')] public function testEnums(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs PHP 8.1'); - } - $this->analyse([__DIR__ . '/data/class-extends-enum.php'], [ [ 'Class ClassExtendsEnum\Foo extends enum ClassExtendsEnum\FooEnum.', @@ -129,12 +129,9 @@ public function testPhpstanInternalClass(): void ]); } + #[RequiresPhp('>= 8.2')] public function testReadonly(): void { - if (PHP_VERSION_ID < 80200) { - $this->markTestSkipped('This test needs PHP 8.2'); - } - $this->analyse([__DIR__ . '/data/extends-readonly-class.php'], [ [ 'Readonly class ExtendsReadOnlyClass\Foo extends non-readonly class ExtendsReadOnlyClass\Nonreadonly.', diff --git a/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php index 4018d2ca62..2045a7d496 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php @@ -7,6 +7,7 @@ use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -14,19 +15,29 @@ class ExistingClassInInstanceOfRuleTest extends RuleTestCase { + private bool $shouldNarrowMethodScopeFromConstructor = true; + protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ExistingClassInInstanceOfRule( $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( @@ -65,6 +76,7 @@ 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'], [ @@ -75,4 +87,27 @@ public function testBug7720(): void ]); } + 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 49b083cd46..a605490cfc 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php @@ -7,7 +7,7 @@ use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,13 +17,16 @@ class ExistingClassInTraitUseRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ExistingClassInTraitUseRule( new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), $reflectionProvider, + true, ); } @@ -69,12 +72,9 @@ public function testTraitUseError(): void ]); } + #[RequiresPhp('>= 8.1')] public function testEnums(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs PHP 8.1'); - } - $this->analyse([__DIR__ . '/data/trait-use-enum.php'], [ [ 'Class TraitUseEnum\Foo uses enum TraitUseEnum\FooEnum.', diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php index 12183ebc85..c3aea0595c 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php @@ -7,7 +7,7 @@ use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,13 +17,16 @@ class ExistingClassesInClassImplementsRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ExistingClassesInClassImplementsRule( new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), $reflectionProvider, + true, ); } @@ -60,12 +63,9 @@ public function testRuleImplementsError(): void ]); } + #[RequiresPhp('>= 8.1')] public function testEnums(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs PHP 8.1'); - } - $this->analyse([__DIR__ . '/data/class-implements-enum.php'], [ [ 'Class ClassImplementsEnum\Foo implements enum ClassImplementsEnum\FooEnum.', diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php index f83d296867..98bf1f3608 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php @@ -7,7 +7,7 @@ use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,23 +17,23 @@ class ExistingClassesInEnumImplementsRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $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 { - if (PHP_VERSION_ID < 80100) { - self::markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/enum-implements.php'], [ [ 'Interface EnumImplements\FooInterface referenced with incorrect case: EnumImplements\FOOInterface.', diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php index cadda3b992..18ecc90324 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php @@ -7,7 +7,7 @@ use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,13 +17,16 @@ class ExistingClassesInInterfaceExtendsRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ExistingClassesInInterfaceExtendsRule( new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), $reflectionProvider, + true, ); } @@ -56,12 +59,9 @@ public function testRuleExtendsError(): void ]); } + #[RequiresPhp('>= 8.1')] public function testEnums(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs PHP 8.1'); - } - $this->analyse([__DIR__ . '/data/interface-extends-enum.php'], [ [ 'Interface InterfaceExtendsEnum\Foo extends enum InterfaceExtendsEnum\FooEnum.', diff --git a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php index 9d3ea64034..f9cdd6ff9b 100644 --- a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -23,14 +22,18 @@ class ForbiddenNameCheckExtensionRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new InstantiationRule( + self::getContainer(), $reflectionProvider, - new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), + new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), + true, ); } diff --git a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php index 562401245b..ffddab32c4 100644 --- a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php @@ -4,7 +4,8 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -12,15 +13,17 @@ class ImpossibleInstanceOfRuleTest extends RuleTestCase { - private bool $checkAlwaysTrueInstanceOf; - private bool $treatPhpDocTypesAsCertain; private bool $reportAlwaysTrueInLastCondition = false; protected function getRule(): Rule { - return new ImpossibleInstanceOfRule($this->checkAlwaysTrueInstanceOf, $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition); + return new ImpossibleInstanceOfRule( + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -30,7 +33,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( @@ -162,11 +164,6 @@ public function testInstanceof(): void 388, $tipText, ], - [ - 'Instanceof between T of Exception and Error will always evaluate to false.', - 404, - $tipText, - ], [ 'Instanceof between class-string and DateTimeInterface will always evaluate to false.', 418, @@ -191,107 +188,8 @@ public function testInstanceof(): void ); } - public function testInstanceofWithoutAlwaysTrue(): void - { - $this->checkAlwaysTrueInstanceOf = false; - $this->treatPhpDocTypesAsCertain = true; - - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->analyse( - [__DIR__ . '/data/impossible-instanceof.php'], - [ - [ - 'Instanceof between ImpossibleInstanceOf\Dolor and ImpossibleInstanceOf\Lorem will always evaluate to false.', - 71, - ], - [ - 'Instanceof between string and ImpossibleInstanceOf\Foo will always evaluate to false.', - 94, - ], - [ - 'Instanceof between string and \'str\' will always evaluate to false.', - 98, - ], - [ - 'Instanceof between ImpossibleInstanceOf\Test|null and ImpossibleInstanceOf\Lorem will always evaluate to false.', - 119, - ], - [ - 'Instanceof between ImpossibleInstanceOf\Test|null and ImpossibleInstanceOf\Lorem will always evaluate to false.', - 137, - ], - [ - 'Instanceof between ImpossibleInstanceOf\Test|null and ImpossibleInstanceOf\Lorem will always evaluate to false.', - 155, - ], - [ - 'Instanceof between callable and ImpossibleInstanceOf\FinalClassWithoutInvoke will always evaluate to false.', - 204, - ], - [ - 'Instanceof between *NEVER* and ImpossibleInstanceOf\Lorem will always evaluate to false.', - 228, - ], - [ - 'Instanceof between *NEVER* and ImpossibleInstanceOf\Foo will always evaluate to false.', - 234, - ], - [ - 'Instanceof between *NEVER* and ImpossibleInstanceOf\Bar will always evaluate to false.', - 240, - //$tipText, - ], - [ - 'Instanceof between object and Exception will always evaluate to false.', - 303, - ], - [ - 'Instanceof between object and InvalidArgumentException will always evaluate to false.', - 307, - ], - [ - 'Instanceof between ImpossibleInstanceOf\Bar and ImpossibleInstanceOf\BarChild will always evaluate to false.', - 318, - ], - [ - 'Instanceof between ImpossibleInstanceOf\Bar and ImpossibleInstanceOf\BarGrandChild will always evaluate to false.', - 322, - ], - /*[ - 'Instanceof between mixed and int results in an error.', - 353, - ], - [ - 'Instanceof between mixed and ImpossibleInstanceOf\InvalidTypeTest|int results in an error.', - 362, - ],*/ - [ - 'Instanceof between T of Exception and Error will always evaluate to false.', - 404, - $tipText, - ], - [ - 'Instanceof between class-string and DateTimeInterface will always evaluate to false.', - 418, - $tipText, - ], - [ - 'Instanceof between class-string and class-string will always evaluate to false.', - 419, - $tipText, - ], - [ - 'Instanceof between class-string and \'DateTimeInterface\' 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'], [ [ @@ -315,7 +213,6 @@ public function testDoNotReportTypesFromPhpDocs(): void public function testReportTypesFromPhpDocs(): void { - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/impossible-instanceof-not-phpdoc.php'], [ [ @@ -349,32 +246,25 @@ 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->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6213.php'], []); } public function testBug5333(): void { - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/bug-5333.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug8042(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('This test needs PHP 8.0'); - } - - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8042.php'], [ [ @@ -390,20 +280,15 @@ public function testBug8042(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug7721(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs PHP 8.1'); - } - - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7721.php'], []); } public function testUnreachableIfBranches(): void { - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../Comparison/data/unreachable-if-branches.php'], [ [ @@ -428,7 +313,6 @@ public function testUnreachableIfBranches(): void public function testIfBranchesDoNotReportPhpDoc(): void { - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/../Comparison/data/unreachable-if-branches-not-phpdoc.php'], [ [ @@ -450,7 +334,6 @@ public function testIfBranchesDoNotReportPhpDoc(): void public function testIfBranchesReportPhpDoc(): 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([__DIR__ . '/../Comparison/data/unreachable-if-branches-not-phpdoc.php'], [ @@ -488,7 +371,6 @@ public function testIfBranchesReportPhpDoc(): void public function testUnreachableTernaryElse(): void { - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../Comparison/data/unreachable-ternary-else-branch.php'], [ [ @@ -504,7 +386,6 @@ public function testUnreachableTernaryElse(): void public function testTernaryElseDoNotReportPhpDoc(): void { - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/../Comparison/data/unreachable-ternary-else-branch-not-phpdoc.php'], [ [ @@ -524,7 +405,6 @@ public function testTernaryElseDoNotReportPhpDoc(): void public function testTernaryElseReportPhpDoc(): 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([__DIR__ . '/../Comparison/data/unreachable-ternary-else-branch-not-phpdoc.php'], [ @@ -550,12 +430,11 @@ public function testTernaryElseReportPhpDoc(): void public function testBug4689(): void { - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/bug-4689.php'], []); } - public function dataReportAlwaysTrueInLastCondition(): iterable + public static function dataReportAlwaysTrueInLastCondition(): iterable { yield [false, [ [ @@ -581,24 +460,19 @@ public function dataReportAlwaysTrueInLastCondition(): iterable } /** - * @dataProvider dataReportAlwaysTrueInLastCondition * @param list $expectedErrors */ + #[DataProvider('dataReportAlwaysTrueInLastCondition')] public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void { - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = true; $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; $this->analyse([__DIR__ . '/data/impossible-instanceof-report-always-true-last-condition.php'], $expectedErrors); } + #[RequiresPhp('>= 8.1')] public function testBug10201(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs PHP 8.1'); - } - - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10201.php'], [ [ @@ -610,7 +484,6 @@ public function testBug10201(): void public function testBug3632(): 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%.'; @@ -623,4 +496,40 @@ public function testBug3632(): void ]); } + #[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, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 36dc32c352..9dabcb1903 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -13,7 +12,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -23,14 +22,18 @@ class InstantiationRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new InstantiationRule( + self::getContainer(), $reflectionProvider, - new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), + new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), + true, ); } @@ -228,12 +231,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.', @@ -246,12 +246,9 @@ public function testOldStyleConstructorOnPhp8(): void ]); } + #[RequiresPhp('< 8.0')] public function testOldStyleConstructorOnPhp7(): void { - if (PHP_VERSION_ID >= 80000) { - $this->markTestSkipped('Test requires PHP 7.x'); - } - $this->analyse([__DIR__ . '/data/php80-constructor.php'], [ [ 'Class OldStyleConstructorOnPhp8 constructor invoked with 0 parameters, 1 required.', @@ -288,6 +285,7 @@ public function testBug4056(): void $this->analyse([__DIR__ . '/data/bug-4056.php'], []); } + #[RequiresPhp('>= 8.0')] public function testNamedArguments(): void { $this->analyse([__DIR__ . '/data/instantiation-named-arguments.php'], [ @@ -346,22 +344,16 @@ public function testBug4681(): void $this->analyse([__DIR__ . '/data/bug-4681.php'], []); } + #[RequiresPhp('>= 8.1')] public function testFirstClassCallable(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1 and static reflection.'); - } - // handled by a different rule $this->analyse([__DIR__ . '/data/first-class-instantiation-callable.php'], []); } + #[RequiresPhp('>= 8.1')] public function testEnumInstantiation(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/enum-instantiation.php'], [ [ 'Cannot instantiate enum EnumInstantiation\Foo.', @@ -388,28 +380,22 @@ public function testBug6370(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug5553(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/bug-5553.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug7048(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/bug-7048.php'], [ [ 'Unknown parameter $recurrences in call to DatePeriod constructor.', 21, ], [ - 'Missing parameter $end (DateTimeInterface|int) in call to DatePeriod constructor.', + 'Missing parameter $end (int|TEnd of DateTimeInterface) in call to DatePeriod constructor.', 18, ], [ @@ -421,31 +407,24 @@ public function testBug7048(): void 24, ], [ - 'Parameter #3 $end of class DatePeriod constructor expects DateTimeInterface|int, string given.', + 'Parameter #3 $end of class DatePeriod constructor expects int|TEnd of DateTimeInterface, string given.', 41, ], [ - 'Parameter $end of class DatePeriod constructor expects DateTimeInterface|int, string given.', + 'Parameter $end of class DatePeriod constructor expects int|TEnd of DateTimeInterface, string given.', 49, ], ]); } + #[RequiresPhp('>= 8.0')] public function testBug7594(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/bug-7594.php'], []); } public function testBug3311a(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/bug-3311a.php'], [ [ 'Parameter #1 $bar of class Bug3311a\Foo constructor expects list, array{1: \'baz\'} given.', @@ -503,4 +482,84 @@ 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'], []); + } + } diff --git a/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php b/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php index de80ea8f95..f58e3fa475 100644 --- a/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php @@ -5,7 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -94,14 +94,23 @@ public function testSupportedOnPhp8(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug9577(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->phpVersion = 80100; $this->analyse([__DIR__ . '/data/bug-9577.php'], []); } + #[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 e55f429317..b08d3804bf 100644 --- a/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php @@ -3,9 +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 const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -15,11 +21,25 @@ class LocalTypeAliasesRuleTest extends RuleTestCase protected function getRule(): Rule { + $reflectionProvider = self::createReflectionProvider(); + return new LocalTypeAliasesRule( new LocalTypeAliasesCheck( ['GlobalTypeAlias' => 'int|string'], - $this->createReflectionProvider(), + 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, ), ); } @@ -91,15 +111,46 @@ 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 { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs PHP 8.1'); - } - $this->analyse([__DIR__ . '/data/local-type-aliases-enums.php'], [ [ 'Cannot import type alias Test: class LocalTypeAliasesEnums\NonexistentClass does not exist.', diff --git a/tests/PHPStan/Rules/Classes/LocalTypeTraitAliasesRuleTest.php b/tests/PHPStan/Rules/Classes/LocalTypeTraitAliasesRuleTest.php index 49d73e9c5c..bd93fd00a2 100644 --- a/tests/PHPStan/Rules/Classes/LocalTypeTraitAliasesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/LocalTypeTraitAliasesRuleTest.php @@ -3,6 +3,12 @@ namespace PHPStan\Rules\Classes; use PHPStan\PhpDoc\TypeNodeResolver; +use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; +use PHPStan\Rules\Generics\GenericObjectTypeCheck; +use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -14,13 +20,27 @@ class LocalTypeTraitAliasesRuleTest extends RuleTestCase protected function getRule(): Rule { + $reflectionProvider = self::createReflectionProvider(); + return new LocalTypeTraitAliasesRule( new LocalTypeAliasesCheck( ['GlobalTypeAlias' => 'int|string'], - $this->createReflectionProvider(), + 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, ), - $this->createReflectionProvider(), + self::createReflectionProvider(), ); } @@ -91,7 +111,17 @@ public function testRule(): void '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 a396245146..7cbd36cda4 100644 --- a/tests/PHPStan/Rules/Classes/MixinRuleTest.php +++ b/tests/PHPStan/Rules/Classes/MixinRuleTest.php @@ -10,7 +10,7 @@ use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -20,18 +20,24 @@ class MixinRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new MixinRule( - $reflectionProvider, - new ClassNameCheck( - new ClassCaseSensitivityCheck($reflectionProvider, true), - new ClassForbiddenNameCheck(self::getContainer()), + 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, ), - new GenericObjectTypeCheck(), - new MissingTypehintCheck(true, true, true, true, []), - new UnresolvableTypeHelper(), - true, ); } @@ -97,15 +103,21 @@ public function testRule(): void 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 { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs PHP 8.1'); - } - $this->analyse([__DIR__ . '/data/mixin-enums.php'], [ [ 'PHPDoc tag @mixin contains non-object type int.', 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 23501ec856..27ad2a84b6 100644 --- a/tests/PHPStan/Rules/Classes/NewStaticRuleTest.php +++ b/tests/PHPStan/Rules/Classes/NewStaticRuleTest.php @@ -2,8 +2,10 @@ namespace PHPStan\Rules\Classes; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -13,7 +15,9 @@ class NewStaticRuleTest extends RuleTestCase protected function getRule(): Rule { - return new NewStaticRule(); + return new NewStaticRule( + new PhpVersion(PHP_VERSION_ID), + ); } public function testRule(): void @@ -39,4 +43,23 @@ 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); + } + } diff --git a/tests/PHPStan/Rules/Classes/NonClassAttributeClassRuleTest.php b/tests/PHPStan/Rules/Classes/NonClassAttributeClassRuleTest.php index f3e2b90cae..1b7901e273 100644 --- a/tests/PHPStan/Rules/Classes/NonClassAttributeClassRuleTest.php +++ b/tests/PHPStan/Rules/Classes/NonClassAttributeClassRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -39,12 +39,9 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1')] public function testEnums(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs PHP 8.1'); - } - $this->analyse([__DIR__ . '/data/enum-cannot-be-attribute.php'], [ [ 'Enum cannot be an Attribute class.', 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/RequireExtendsRuleTest.php b/tests/PHPStan/Rules/Classes/RequireExtendsRuleTest.php index 0ca40d9fa4..966fac799a 100644 --- a/tests/PHPStan/Rules/Classes/RequireExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/RequireExtendsRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,12 +17,9 @@ protected function getRule(): Rule return new RequireExtendsRule(); } + #[RequiresPhp('>= 8.1')] public function testRule(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1'); - } - $expectedErrors = [ [ 'Trait IncompatibleRequireExtends\ValidTrait requires using class to extend IncompatibleRequireExtends\SomeClass, but IncompatibleRequireExtends\InValidTraitUse2 does not.', @@ -52,6 +49,22 @@ public function testRule(): void '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); diff --git a/tests/PHPStan/Rules/Classes/RequireImplementsRuleTest.php b/tests/PHPStan/Rules/Classes/RequireImplementsRuleTest.php index a2fe9cf7a3..41d2668a4b 100644 --- a/tests/PHPStan/Rules/Classes/RequireImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/RequireImplementsRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,12 +17,9 @@ protected function getRule(): Rule return new RequireImplementsRule(); } + #[RequiresPhp('>= 8.1')] public function testRule(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1'); - } - $expectedErrors = [ [ 'Trait IncompatibleRequireImplements\ValidTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface, but IncompatibleRequireImplements\InValidTraitUse2 does not.', @@ -57,7 +54,7 @@ public function testRule(): void 137, ], [ - 'Trait IncompatibleRequireImplements\ValidPsalmTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface2, but class@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php:164 does not.', + '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, ], [ diff --git a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php index b6530920df..3550be5799 100644 --- a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php +++ b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php @@ -12,15 +12,19 @@ class UnusedConstructorParametersRuleTest extends RuleTestCase { + 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.', @@ -33,6 +37,20 @@ public function testUnusedConstructorParameters(): void ]); } + public function testUnusedConstructorParameters(): void + { + $this->analyse([__DIR__ . '/data/unused-constructor-parameters.php'], [ + [ + 'Constructor of class UnusedConstructorParameters\Foo has an unused parameter $unusedParameter.', + 19, + ], + [ + 'Constructor of class UnusedConstructorParameters\Foo has an unused parameter $anotherUnusedParameter.', + 20, + ], + ]); + } + public function testPromotedProperties(): void { $this->analyse([__DIR__ . '/data/unused-constructor-parameters-promoted-properties.php'], []); @@ -43,9 +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/bug-11454.php b/tests/PHPStan/Rules/Classes/data/bug-11454.php new file mode 100644 index 0000000000..1a7fe447d1 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-11454.php @@ -0,0 +1,14 @@ + withTrashed(bool $withTrashed = true) + * @method static Builder onlyTrashed() + * @method static Builder withoutTrashed() + * @method static bool restore() + * @method static static restoreOrCreate(array $attributes = [], array $values = []) + * @method static static createOrRestore(array $attributes = [], array $values = []) + */ +trait SoftDeletes {} + +function test(): void { + assertType('Bug11591MethodTag\\Builder', User::withTrashed()); + assertType('Bug11591MethodTag\\Builder', User::onlyTrashed()); + assertType('Bug11591MethodTag\\Builder', User::withoutTrashed()); + assertType(User::class, User::createOrRestore()); + assertType(User::class, User::restoreOrCreate()); +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-11591-property-tag.php b/tests/PHPStan/Rules/Classes/data/bug-11591-property-tag.php new file mode 100644 index 0000000000..c9bf36e246 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-11591-property-tag.php @@ -0,0 +1,26 @@ + $a + * @property static $b + */ +trait SoftDeletes {} + +function test(User $user): void { + assertType('Bug11591PropertyTag\\Builder', $user->a); + assertType(User::class, $user->b); +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-11591.php b/tests/PHPStan/Rules/Classes/data/bug-11591.php new file mode 100644 index 0000000000..e41413653a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-11591.php @@ -0,0 +1,44 @@ + + */ +trait WithConfig { + /** + * @param SettingsFactory $settings + */ + public function setConfig(callable $settings): void { + $settings($this); + } + + /** + * @param callable(static): array $settings + */ + public function setConfig2(callable $settings): void { + $settings($this); + } + + /** + * @param callable(self): array $settings + */ + public function setConfig3(callable $settings): void { + $settings($this); + } +} + +class A +{ + use WithConfig; +} + +function (A $a): void { + $a->setConfig(function ($who) { + assertType(A::class, $who); + + return []; + }); +}; diff --git a/tests/PHPStan/Rules/Classes/data/bug-11592.php b/tests/PHPStan/Rules/Classes/data/bug-11592.php new file mode 100644 index 0000000000..b94251a495 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-11592.php @@ -0,0 +1,47 @@ += 8.1 + +namespace Bug11592; + +trait HelloWorld +{ + abstract public static function cases(): array; + + abstract public static function from(): self; + + abstract public static function tryFrom(): ?self; +} + +enum Test +{ + use HelloWorld; +} + +enum Test2 +{ + + abstract public static function cases(): array; + + abstract public static function from(): self; + + abstract public static function tryFrom(): ?self; + +} + +enum BackedTest: int +{ + use HelloWorld; +} + +enum BackedTest2: int +{ + abstract public static function cases(): array; + + abstract public static function from(): self; + + abstract public static function tryFrom(): ?self; +} + +enum EnumWithAbstractMethod +{ + abstract function foo(); +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-11815.php b/tests/PHPStan/Rules/Classes/data/bug-11815.php new file mode 100644 index 0000000000..c08dccb9ea --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-11815.php @@ -0,0 +1,43 @@ += 8.2 + +declare(strict_types = 1); + +class Dimensions +{ + public function __construct( + public int $width, + public int $height, + ) { + } +} + +class StoreProcessorResult +{ + public function __construct( + public string $path, + public string $mimetype, + public Dimensions $dimensions, + public int $filesize, + public true|null $identical = null, + ) { + } +} + +/** + * @return array{path: string, identical?: true} + */ +function getPath(): array +{ + $data = ['path' => 'some/path']; + if ((bool)rand(0, 1)) { + $data['identical'] = true; + } + return $data; +} + +$data = getPath(); +$data['dimensions'] = new Dimensions(100, 100); +$data['mimetype'] = 'image/png'; +$data['filesize'] = 123456; + +$dto = new StoreProcessorResult(...$data); diff --git a/tests/PHPStan/Rules/Classes/data/bug-12011.php b/tests/PHPStan/Rules/Classes/data/bug-12011.php new file mode 100644 index 0000000000..94671eec7a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-12011.php @@ -0,0 +1,27 @@ += 8.3 + +namespace Bug12011; + +use Attribute; + +#[Table(self::TABLE_NAME)] +class HelloWorld +{ + private const string TABLE_NAME = 'table'; +} + +#[Attribute(Attribute::TARGET_CLASS)] +final class Table +{ + public function __construct( + public readonly string|null $name = null, + public readonly string|null $schema = null, + ) { + } +} + +#[Table(self::TABLE_NAME)] +class HelloWorld2 +{ + private const int TABLE_NAME = 1; +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-12281.php b/tests/PHPStan/Rules/Classes/data/bug-12281.php new file mode 100644 index 0000000000..293d9e5e41 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-12281.php @@ -0,0 +1,16 @@ += 8.2 + +namespace Bug12281; + +#[\AllowDynamicProperties] +readonly class BlogData { /* … */ } + +/** @readonly */ +#[\AllowDynamicProperties] +class BlogDataPhpdoc { /* … */ } + +#[\AllowDynamicProperties] +enum BlogDataEnum { /* … */ } + +#[\AllowDynamicProperties] +interface BlogDataInterface { /* … */ } diff --git a/tests/PHPStan/Rules/Classes/data/bug-3311a.php b/tests/PHPStan/Rules/Classes/data/bug-3311a.php index d4f646deb5..bb28ed1eb1 100644 --- a/tests/PHPStan/Rules/Classes/data/bug-3311a.php +++ b/tests/PHPStan/Rules/Classes/data/bug-3311a.php @@ -1,4 +1,4 @@ -= 7.4 +', $foo); - assertNativeType('array', $foo); + assertNativeType('array', $foo); assertType('Bug5333\Route', $res); assertNativeType('Bug5333\Route', $res); 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-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-9946.php b/tests/PHPStan/Rules/Classes/data/bug-9946.php index 839ac021a1..e2cb92f5f2 100644 --- a/tests/PHPStan/Rules/Classes/data/bug-9946.php +++ b/tests/PHPStan/Rules/Classes/data/bug-9946.php @@ -1,4 +1,4 @@ -= 7.4 += 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-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/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/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/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 @@ + */ class Foo { @@ -68,3 +68,39 @@ class InvalidTypeDefinitionToIgnoreBecauseItsAParseErrorAlreadyReportedInInvalid { } + +/** + * @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 index d8c16b3e0e..6628e0db7c 100644 --- a/tests/PHPStan/Rules/Classes/data/local-type-trait-aliases.php +++ b/tests/PHPStan/Rules/Classes/data/local-type-trait-aliases.php @@ -5,7 +5,7 @@ class ExistingClassAlias {} /** - * @phpstan-type ExportedTypeAlias \Countable&\Traversable + * @phpstan-type ExportedTypeAlias \Countable&\Traversable */ trait Foo { @@ -62,3 +62,24 @@ trait Generic trait Invalid { } + +/** + * @phpstan-type NoIterablueValue = array + */ +trait MissingType +{ + +} + +class Usages +{ + + use Foo; + use Bar; + use Baz; + use Qux; + use Generic; + use Invalid; + use MissingType; + +} diff --git a/tests/PHPStan/Rules/Classes/data/local-type-trait-use-aliases.php b/tests/PHPStan/Rules/Classes/data/local-type-trait-use-aliases.php new file mode 100644 index 0000000000..94e019280c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/local-type-trait-use-aliases.php @@ -0,0 +1,26 @@ + + */ +trait Foo +{ + +} + +class Usage +{ + + use Foo; + +} diff --git a/tests/PHPStan/Rules/Classes/data/method-tag-trait-enum.php b/tests/PHPStan/Rules/Classes/data/method-tag-trait-enum.php new file mode 100644 index 0000000000..9855c17844 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/method-tag-trait-enum.php @@ -0,0 +1,18 @@ += 8.1 + +namespace MethodTagTraitEnum; + +/** + * @method intt doFoo() + */ +trait Foo +{ + +} + +enum FooEnum +{ + + use Foo; + +} diff --git a/tests/PHPStan/Rules/Classes/data/method-tag-trait.php b/tests/PHPStan/Rules/Classes/data/method-tag-trait.php new file mode 100644 index 0000000000..504033696a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/method-tag-trait.php @@ -0,0 +1,30 @@ + doA() + * @method Generic doB() + * @method Generic doC() + * @method Generic doD() + */ +class TestGenerics +{ + +} + +/** + * @method Generic doA() + */ +class MissingGenerics +{ + +} + +/** + * @method Generic doA() + */ +class MissingIterableValue +{ + +} + +/** + * @method Generic doA() + */ +class MissingCallableSignature +{ + +} + +/** + * @method Nonexistent doA() + * @method \PropertyTagTrait\Foo doB() + * @method fOO doC() + */ +class NonexistentClasses +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/mixin-trait-use.php b/tests/PHPStan/Rules/Classes/data/mixin-trait-use.php new file mode 100644 index 0000000000..0b67bfb4fb --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/mixin-trait-use.php @@ -0,0 +1,36 @@ + + * @mixin string&int + */ +trait FooTrait +{ + +} + +class Usages +{ + + use FooTrait; + +} + +function (Usages $u): void { + assertType(Usages::class, $u->get()); +}; diff --git a/tests/PHPStan/Rules/Classes/data/mixin-trait.php b/tests/PHPStan/Rules/Classes/data/mixin-trait.php new file mode 100644 index 0000000000..83c0f0b488 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/mixin-trait.php @@ -0,0 +1,17 @@ + + */ +trait FooTrait +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/mixin.php b/tests/PHPStan/Rules/Classes/data/mixin.php index d5a3fafd67..b6ab1b092b 100644 --- a/tests/PHPStan/Rules/Classes/data/mixin.php +++ b/tests/PHPStan/Rules/Classes/data/mixin.php @@ -117,3 +117,19 @@ 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-in-abstract-class-static-method.php b/tests/PHPStan/Rules/Classes/data/new-static-in-abstract-class-static-method.php new file mode 100644 index 0000000000..bbcfea8ef8 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/new-static-in-abstract-class-static-method.php @@ -0,0 +1,33 @@ += 7.4 + $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/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 @@ + @@ -13,8 +14,6 @@ class BooleanAndConstantConditionRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; - private bool $bleedingEdge = false; - private bool $reportAlwaysTrueInLastCondition = false; protected function getRule(): Rule @@ -22,18 +21,16 @@ protected function getRule(): Rule return new BooleanAndConstantConditionRule( new ConstantConditionRuleHelper( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, - $this->bleedingEdge, $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -139,90 +136,6 @@ 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 && is always true.', - 15, - ], - [ - 'Right side of && is always true.', - 19, - ], - [ - 'Left side of && is always false.', - 24, - ], - [ - 'Right side of && is always false.', - 27, - ], - [ - 'Result of && is always false.', - 30, - ], - [ - 'Right side of && is always true.', - 33, - ], - [ - 'Right side of && is always true.', - 36, - ], - [ - 'Right side of && is always true.', - 39, - ], - [ - 'Result of && is always false.', - 50, - ], - [ - 'Result of && is always true.', - 54, - $tipText, - ], - [ - 'Result of && is always false.', - 60, - ], - [ - 'Result of && is always true.', - 64, - //$tipText, - ], - [ - 'Result of && is always false.', - 66, - //$tipText, - ], - [ - 'Result of && is always false.', - 125, - ], - [ - 'Left side of && is always false.', - 139, - ], - [ - 'Right side of && is always false.', - 141, - ], - [ - 'Left side of && is always true.', - 145, - ], - [ - 'Right side of && is always true.', - 147, - ], - ]); - } - - public function testRuleLogicalAndBleedingEdge(): void - { - $this->treatPhpDocTypesAsCertain = true; - $this->bleedingEdge = 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.', @@ -348,7 +261,7 @@ public function testReportPhpDoc(): void ]); } - public function dataTreatPhpDocTypesAsCertainRegression(): array + public static function dataTreatPhpDocTypesAsCertainRegression(): array { return [ [ @@ -360,9 +273,7 @@ public function dataTreatPhpDocTypesAsCertainRegression(): array ]; } - /** - * @dataProvider dataTreatPhpDocTypesAsCertainRegression - */ + #[DataProvider('dataTreatPhpDocTypesAsCertainRegression')] public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAsCertain): void { $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; @@ -432,7 +343,7 @@ public function testBug5743(): void $this->analyse([__DIR__ . '/data/bug-5743.php'], []); } - public function dataBug4969(): iterable + public static function dataBug4969(): iterable { yield [false, []]; yield [true, [ @@ -445,16 +356,16 @@ public function dataBug4969(): iterable } /** - * @dataProvider dataBug4969 * @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 function dataReportAlwaysTrueInLastCondition(): iterable + public static function dataReportAlwaysTrueInLastCondition(): iterable { yield [false, [ [ @@ -504,9 +415,9 @@ public function dataReportAlwaysTrueInLastCondition(): iterable } /** - * @dataProvider dataReportAlwaysTrueInLastCondition * @param list $expectedErrors */ + #[DataProvider('dataReportAlwaysTrueInLastCondition')] public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void { $this->treatPhpDocTypesAsCertain = true; @@ -521,4 +432,17 @@ public function testBug5365(): void $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 43ecf911a6..24df1d9ea1 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\DataProvider; /** * @extends RuleTestCase @@ -21,17 +21,16 @@ protected function getRule(): Rule return new BooleanNotConstantConditionRule( new ConstantConditionRuleHelper( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -103,7 +102,7 @@ public function testReportPhpDoc(): void ]); } - public function dataTreatPhpDocTypesAsCertainRegression(): array + public static function dataTreatPhpDocTypesAsCertainRegression(): array { return [ [ @@ -115,9 +114,7 @@ public function dataTreatPhpDocTypesAsCertainRegression(): array ]; } - /** - * @dataProvider dataTreatPhpDocTypesAsCertainRegression - */ + #[DataProvider('dataTreatPhpDocTypesAsCertainRegression')] public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAsCertain): void { $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; @@ -126,10 +123,6 @@ public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAs public function testBug6473(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6473.php'], []); } @@ -152,7 +145,13 @@ public function testBug8797(): void $this->analyse([__DIR__ . '/data/bug-8797.php'], []); } - public function dataReportAlwaysTrueInLastCondition(): iterable + public function testBug7937(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7937.php'], []); + } + + public static function dataReportAlwaysTrueInLastCondition(): iterable { yield [false, [ [ @@ -190,9 +189,9 @@ public function dataReportAlwaysTrueInLastCondition(): iterable } /** - * @dataProvider dataReportAlwaysTrueInLastCondition * @param list $expectedErrors */ + #[DataProvider('dataReportAlwaysTrueInLastCondition')] public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php index ee616efbc5..a1a399005e 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php @@ -4,7 +4,8 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -14,8 +15,6 @@ class BooleanOrConstantConditionRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; - private bool $bleedingEdge = false; - private bool $reportAlwaysTrueInLastCondition = false; protected function getRule(): Rule @@ -23,18 +22,16 @@ protected function getRule(): Rule return new BooleanOrConstantConditionRule( new ConstantConditionRuleHelper( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, - $this->bleedingEdge, $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -131,81 +128,6 @@ 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 || is always true.', - 15, - ], - [ - 'Right side of || is always true.', - 19, - ], - [ - 'Left side of || is always false.', - 24, - ], - [ - 'Right side of || is always false.', - 27, - ], - [ - 'Right side of || is always true.', - 30, - ], - [ - 'Result of || is always true.', - 33, - ], - [ - 'Right side of || is always false.', - 36, - ], - [ - 'Right side of || is always false.', - 39, - ], - [ - 'Result of || is always true.', - 50, - $tipText, - ], - [ - 'Result of || is always true.', - 54, - $tipText, - ], - [ - 'Result of || is always true.', - 61, - ], - [ - 'Result of || is always true.', - 65, - ], - [ - 'Left side of || is always false.', - 77, - ], - [ - 'Right side of || is always false.', - 79, - ], - [ - 'Left side of || is always true.', - 83, - ], - [ - 'Right side of || is always true.', - 85, - ], - ]); - } - - public function testRuleLogicalOrBleedingEdge(): void - { - $this->treatPhpDocTypesAsCertain = true; - $this->bleedingEdge = 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.', @@ -322,7 +244,7 @@ public function testReportPhpDoc(): void ]); } - public function dataTreatPhpDocTypesAsCertainRegression(): array + public static function dataTreatPhpDocTypesAsCertainRegression(): array { return [ [ @@ -334,21 +256,16 @@ public function dataTreatPhpDocTypesAsCertainRegression(): array ]; } - /** - * @dataProvider dataTreatPhpDocTypesAsCertainRegression - */ + #[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 { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6258.php'], []); } @@ -371,7 +288,7 @@ public function testBug7881(): void $this->analyse([__DIR__ . '/data/bug-7881.php'], []); } - public function dataReportAlwaysTrueInLastCondition(): iterable + public static function dataReportAlwaysTrueInLastCondition(): iterable { yield [false, [ [ @@ -421,9 +338,9 @@ public function dataReportAlwaysTrueInLastCondition(): iterable } /** - * @dataProvider dataReportAlwaysTrueInLastCondition * @param list $expectedErrors */ + #[DataProvider('dataReportAlwaysTrueInLastCondition')] public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php index f8a0b7d87d..e1a9a3db03 100644 --- a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php @@ -4,6 +4,9 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; +use function array_merge; use const PHP_VERSION_ID; /** @@ -12,44 +15,21 @@ class ConstantLooseComparisonRuleTest extends RuleTestCase { - private bool $checkAlwaysTrueStrictComparison; - private bool $treatPhpDocTypesAsCertain = true; private bool $reportAlwaysTrueInLastCondition = false; protected function getRule(): Rule { - return new ConstantLooseComparisonRule($this->checkAlwaysTrueStrictComparison, $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition); + return new ConstantLooseComparisonRule( + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ); } public function testRule(): void { - $this->checkAlwaysTrueStrictComparison = false; - $this->analyse([__DIR__ . '/data/loose-comparison.php'], [ - [ - "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 3 and 3 will always evaluate to false.', - 48, - 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', - ], - ]); - } - - public function testRuleAlwaysTrue(): void - { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/loose-comparison.php'], [ [ "Loose comparison using == between 0 and '0' will always evaluate to true.", @@ -80,13 +60,9 @@ public function testRuleAlwaysTrue(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug8485(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-8485.php'], [ [ 'Loose comparison using == between Bug8485\E::c and Bug8485\E::c will always evaluate to true.', @@ -111,7 +87,7 @@ public function testBug8485(): void ]); } - public function dataReportAlwaysTrueInLastCondition(): iterable + public static function dataReportAlwaysTrueInLastCondition(): iterable { yield [false, [ [ @@ -133,17 +109,16 @@ public function dataReportAlwaysTrueInLastCondition(): iterable } /** - * @dataProvider dataReportAlwaysTrueInLastCondition * @param list $expectedErrors */ + #[DataProvider('dataReportAlwaysTrueInLastCondition')] public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void { - $this->checkAlwaysTrueStrictComparison = true; $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; $this->analyse([__DIR__ . '/data/loose-comparison-report-always-true-last-condition.php'], $expectedErrors); } - public function dataTreatPhpDocTypesAsCertain(): iterable + public static function dataTreatPhpDocTypesAsCertain(): iterable { yield [false, []]; yield [true, [ @@ -156,14 +131,120 @@ public function dataTreatPhpDocTypesAsCertain(): iterable } /** - * @dataProvider dataTreatPhpDocTypesAsCertain * @param list $expectedErrors */ + #[DataProvider('dataTreatPhpDocTypesAsCertain')] public function testTreatPhpDocTypesAsCertain(bool $treatPhpDocTypesAsCertain, array $expectedErrors): void { - $this->checkAlwaysTrueStrictComparison = true; $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; $this->analyse([__DIR__ . '/data/loose-comparison-treat-phpdoc-types.php'], $expectedErrors); } + 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 703c2e5a87..cbfdf67cad 100644 --- a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php @@ -18,16 +18,15 @@ protected function getRule(): Rule return new DoWhileLoopConstantConditionRule( new ConstantConditionRuleHelper( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, + true, ); } diff --git a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php index 7d3b008d90..9eec3deec1 100644 --- a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -20,17 +22,16 @@ protected function getRule(): Rule return new ElseIfConstantConditionRule( new ConstantConditionRuleHelper( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -39,7 +40,7 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } - public function dataRule(): iterable + public static function dataRule(): iterable { yield [false, [ [ @@ -82,9 +83,9 @@ public function dataRule(): iterable } /** - * @dataProvider dataRule * @param list $expectedErrors */ + #[DataProvider('dataRule')] public function testRule(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void { $this->treatPhpDocTypesAsCertain = true; @@ -121,4 +122,33 @@ public function testReportPhpDoc(): void ]); } + #[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 a7c2629d16..eee1500835 100644 --- a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -19,16 +19,15 @@ protected function getRule(): Rule return new IfConstantConditionRule( new ConstantConditionRuleHelper( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, + true, ); } @@ -132,12 +131,9 @@ public function testBug6902(): void $this->analyse([__DIR__ . '/data/bug-6902.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug8485(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->treatPhpDocTypesAsCertain = true; // reported by ConstantLooseComparisonRule instead @@ -168,4 +164,22 @@ public function testBug10561(): void $this->analyse([__DIR__ . '/data/bug-10561.php'], []); } + public function testBug4912(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4912.php'], []); + } + + public function testBug4864(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4864.php'], []); + } + + public function testBug8926(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8926.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 88d9315d84..132ec6bfa2 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -4,12 +4,13 @@ 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; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -17,8 +18,6 @@ class ImpossibleCheckTypeFunctionCallRuleTest extends RuleTestCase { - private bool $checkAlwaysTrueCheckTypeFunctionCall; - private bool $treatPhpDocTypesAsCertain; private bool $reportAlwaysTrueInLastCondition = false; @@ -27,15 +26,14 @@ protected function getRule(): Rule { return new ImpossibleCheckTypeFunctionCallRule( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [stdClass::class], $this->treatPhpDocTypesAsCertain, - true, ), - $this->checkAlwaysTrueCheckTypeFunctionCall, $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -44,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'], @@ -92,173 +90,189 @@ 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.', - 210, + 220, ], [ 'Call to function in_array() with arguments int, array{\'foo\', \'bar\'} and true will always evaluate to false.', - 236, + 246, ], [ 'Call to function in_array() with arguments \'bar\'|\'foo\', array{\'baz\', \'lorem\'} and true will always evaluate to false.', - 245, + 255, ], [ 'Call to function in_array() with arguments \'foo\', array{\'foo\'} and true will always evaluate to true.', - 253, + 263, ], [ 'Call to function in_array() with arguments \'foo\', array{\'foo\', \'bar\'} and true will always evaluate to true.', - 257, + 267, ], [ 'Call to function in_array() with arguments \'bar\', array{}|array{\'foo\'} and true will always evaluate to false.', - 321, + 331, ], [ 'Call to function in_array() with arguments \'baz\', array{0: \'bar\', 1?: \'foo\'} and true will always evaluate to false.', - 337, + 347, ], [ 'Call to function in_array() with arguments \'foo\', array{} and true will always evaluate to false.', - 344, + 354, ], [ 'Call to function array_key_exists() with \'a\' and array{a: 1, b?: 2} will always evaluate to true.', - 361, + 371, ], [ 'Call to function array_key_exists() with \'c\' and array{a: 1, b?: 2} will always evaluate to false.', - 367, + 377, ], [ 'Call to function is_string() with mixed will always evaluate to false.', - 561, + 571, ], [ 'Call to function is_callable() with mixed will always evaluate to false.', - 572, + 582, ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExists\' and \'testWithStringFirst…\' will always evaluate to true.', - 586, + 596, ], [ 'Call to function method_exists() with \'UndefinedClass\' and string will always evaluate to false.', - 595, + 605, ], [ 'Call to function method_exists() with \'UndefinedClass\' and \'test\' will always evaluate to false.', - 598, + 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.', - 610, + 635, ], [ 'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'method\' will always evaluate to true.', - 625, + 650, ], [ 'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'someAnother\' will always evaluate to true.', - 628, + 653, ], [ 'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'unknown\' will always evaluate to false.', - 631, + 656, ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'method\' will always evaluate to true.', - 634, + 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.', - 637, + 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.', - 640, + 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.', - 643, + 668, ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'someAnother\' will always evaluate to true.', - 646, + 671, ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', - 649, + 674, ], [ 'Call to function is_string() with string will always evaluate to true.', - 678, + 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.', - 693, + 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.', - 693, + 718, ], [ 'Call to function assert() with false will always evaluate to false.', - 694, + 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.', - 694, + 719, ], [ 'Call to function assert() with true will always evaluate to true.', - 701, + 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.', - 701, + 726, ], [ 'Call to function property_exists() with CheckTypeFunctionCall\Bug2221 and \'foo\' will always evaluate to true.', - 784, + 809, ], [ 'Call to function property_exists() with CheckTypeFunctionCall\Bug2221 and \'foo\' will always evaluate to true.', - 788, + 813, ], [ 'Call to function testIsInt() with int will always evaluate to true.', - 875, + 900, ], [ 'Call to function is_int() with int will always evaluate to true.', - 889, + 914, 'Remove remaining cases below this one and this error will disappear too.', ], [ 'Call to function in_array() with arguments 1, array and true will always evaluate to false.', - 927, + 952, 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], ], @@ -267,117 +281,12 @@ public function testImpossibleCheckTypeFunctionCall(): void public function testBug7898(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7898.php'], []); } - 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 in_array() with arguments int, array{\'foo\', \'bar\'} and true will always evaluate to false.', - 236, - ], - [ - 'Call to function in_array() with arguments \'bar\'|\'foo\', array{\'baz\', \'lorem\'} and true will always evaluate to false.', - 245, - ], - [ - 'Call to function in_array() with arguments \'bar\', array{}|array{\'foo\'} and true will always evaluate to false.', - 321, - ], - [ - 'Call to function in_array() with arguments \'baz\', array{0: \'bar\', 1?: \'foo\'} and true will always evaluate to false.', - 337, - ], - [ - 'Call to function in_array() with arguments \'foo\', array{} and true will always evaluate to false.', - 344, - ], - [ - 'Call to function array_key_exists() with \'c\' and array{a: 1, b?: 2} will always evaluate to false.', - 367, - ], - [ - 'Call to function is_string() with mixed will always evaluate to false.', - 561, - ], - [ - 'Call to function is_callable() with mixed will always evaluate to false.', - 572, - ], - [ - 'Call to function method_exists() with \'UndefinedClass\' and string will always evaluate to false.', - 595, - ], - [ - 'Call to function method_exists() with \'UndefinedClass\' and \'test\' will always evaluate to false.', - 598, - ], - [ - 'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'unknown\' will always evaluate to false.', - 631, - ], - [ - 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', - 640, - '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.', - 649, - ], - [ - 'Call to function assert() with false will always evaluate to false.', - 694, - '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.', - 694, - ], - [ - 'Call to function in_array() with arguments 1, array and true will always evaluate to false.', - 927, - 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', - ], - ], - ); - } - public function testDoNotReportTypesFromPhpDocs(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/check-type-function-call-not-phpdoc.php'], [ [ @@ -389,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'], [ [ @@ -416,53 +324,43 @@ public function testReportTypesFromPhpDocs(): void 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 { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/array-is-list.php'], [ [ @@ -483,14 +381,12 @@ public function testArrayIsList(): void public function testBug3766(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3766.php'], []); } public function testBug6305(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6305.php'], [ [ @@ -506,21 +402,18 @@ public function testBug6305(): void public function testBug6698(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6698.php'], []); } public function testBug5369(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-5369.php'], []); } public function testBugInArrayDateFormat(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/in-array-date-format.php'], [ [ @@ -547,70 +440,61 @@ public function testBugInArrayDateFormat(): void public function testBug5496(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-5496.php'], []); } public function testBug3892(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3892.php'], []); } public function testBug3314(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3314.php'], []); } public function testBug2870(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-2870.php'], []); } public function testBug5354(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-5354.php'], []); } public function testSlevomatCsInArrayBug(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/slevomat-cs-in-array.php'], []); } public function testNonEmptySpecifiedString(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/non-empty-string-impossible-type.php'], []); } public function testBug2755(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-2755.php'], []); } public function testBug7079(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7079.php'], []); } + #[RequiresPhp('>= 8.0')] public function testConditionalTypesInference(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/conditional-types-inference.php'], [ [ @@ -638,63 +522,55 @@ public function testConditionalTypesInference(): void public function testBug6697(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6697.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug6443(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6443.php'], []); } public function testBug7684(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = false; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7684.php'], []); } public function testBug7224(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-7224.php'], []); } public function testBug4708(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-4708.php'], []); } public function testBug3821(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3821.php'], []); } public function testBug6599(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6599.php'], []); } public function testBug7914(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7914.php'], []); } public function testDocblockAssertEquality(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/docblock-assert-equality.php'], [ [ @@ -706,63 +582,54 @@ public function testDocblockAssertEquality(): void public function testBug8076(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8076.php'], []); } public function testBug8562(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8562.php'], []); } public function testBug6938(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/bug-6938.php'], []); } public function testBug8727(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8727.php'], []); } public function testBug8474(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8474.php'], []); } public function testBug5695(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/bug-5695.php'], []); } public function testBug8752(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8752.php'], []); } public function testDiscussion9134(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/discussion-9134.php'], []); } public function testImpossibleMethodExistOnGenericClassString(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = 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%.'; @@ -796,7 +663,7 @@ public function testImpossibleMethodExistOnGenericClassString(): void ]); } - public function dataReportAlwaysTrueInLastCondition(): iterable + public static function dataReportAlwaysTrueInLastCondition(): iterable { yield [false, [ [ @@ -818,12 +685,11 @@ public function dataReportAlwaysTrueInLastCondition(): iterable } /** - * @dataProvider dataReportAlwaysTrueInLastCondition * @param list $expectedErrors */ + #[DataProvider('dataReportAlwaysTrueInLastCondition')] public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; $this->analyse([__DIR__ . '/data/impossible-function-report-always-true-last-condition.php'], $expectedErrors); @@ -831,7 +697,6 @@ public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLast public function testObjectShapes(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/property-exists-object-shapes.php'], [ [ @@ -1002,13 +867,9 @@ private static function getLooseComparisonAgainsEnumsIssues(): array ]; } + #[RequiresPhp('>= 8.1')] public function testLooseComparisonAgainstEnums(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $issues = array_map( static function (array $i): array { @@ -1025,20 +886,15 @@ static function (array $i): array { public function testNonStrictInArray(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9662.php'], []); } + #[RequiresPhp('>= 8.1')] public function testNonStrictInArrayEnums(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9662-enums.php'], [ [ @@ -1062,13 +918,9 @@ public function testNonStrictInArrayEnums(): void ]); } + #[RequiresPhp('>= 8.1')] public function testLooseComparisonAgainstEnumsNoPhpdoc(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = false; $issues = self::getLooseComparisonAgainsEnumsIssues(); $issues = array_values(array_filter($issues, static fn (array $i) => count($i) === 2)); @@ -1079,7 +931,6 @@ public function testBug10502(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-10502.php'], [ [ @@ -1096,9 +947,89 @@ public function testBug10502(): void public function testAlwaysTruePregMatch(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $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'], []); + } + + #[RequiresPhp('>= 8.2')] + public function testBug13291(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13291.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php index 9be90b8054..aff6f8d5df 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php @@ -15,15 +15,14 @@ public function getRule(): Rule { return new ImpossibleCheckTypeMethodCallRule( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], true, - true, ), true, - true, false, + true, ); } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php index 3aa4bf0810..2eec07890a 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php @@ -15,15 +15,14 @@ public function getRule(): Rule { return new ImpossibleCheckTypeMethodCallRule( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], true, - true, ), true, - true, false, + true, ); } @@ -41,10 +40,12 @@ public function testRule(): void [ 'Call to method PHPStan\Tests\AssertionClass::assertNotInt() with int will always evaluate to false.', 30, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to method PHPStan\Tests\AssertionClass::assertNotInt() with string will always evaluate to true.', 36, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to method ImpossibleMethodCall\Foo::isSame() with 1 and 1 will always evaluate to true.', diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php index 7a697e6ffa..3fd625a4b1 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -19,15 +21,14 @@ public function getRule(): Rule { return new ImpossibleCheckTypeMethodCallRule( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, - true, ), - true, $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -51,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.', @@ -197,26 +200,27 @@ 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.', - 19, + 21, ], [ 'Call to method Bug8169\HelloWorld::assertString() with string will always evaluate to true.', - 26, + 28, ], [ 'Call to method Bug8169\HelloWorld::assertString() with int will always evaluate to false.', - 33, + 35, ], ]); } - public function dataReportAlwaysTrueInLastCondition(): iterable + public static function dataReportAlwaysTrueInLastCondition(): iterable { yield [false, [ [ @@ -238,9 +242,9 @@ public function dataReportAlwaysTrueInLastCondition(): iterable } /** - * @dataProvider dataReportAlwaysTrueInLastCondition * @param list $expectedErrors */ + #[DataProvider('dataReportAlwaysTrueInLastCondition')] public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void { $this->treatPhpDocTypesAsCertain = true; @@ -248,6 +252,29 @@ public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLast $this->analyse([__DIR__ . '/data/impossible-method-report-always-true-last-condition.php'], $expectedErrors); } + public function testBug12473(): void + { + $this->treatPhpDocTypesAsCertain = true; + $tip = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/bug-12473.php'], [ + /*[ + 'Call to method ReflectionClass::isSubclassOf() with \'Bug12473\\\\Picture\' will always evaluate to true.', + 39, + $tip, + ],*/ + [ + 'Call to method ReflectionClass::isSubclassOf() with \'Bug12473\\\\PictureProduct\' will always evaluate to false.', + 49, + $tip, + ], + /*[ + 'Call to method ReflectionClass::isSubclassOf() with \'Bug12473\\\\PictureUser\' will always evaluate to true.', + 59, + $tip, + ],*/ + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php index a93147ab83..1b43cdea9d 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; /** * @extends RuleTestCase @@ -19,15 +20,14 @@ public function getRule(): Rule { return new ImpossibleCheckTypeStaticMethodCallRule( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, - true, ), - true, $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -113,7 +113,7 @@ public function testAssertUnresolvedGeneric(): void $this->analyse([__DIR__ . '/data/assert-unresolved-generic.php'], []); } - public function dataReportAlwaysTrueInLastCondition(): iterable + public static function dataReportAlwaysTrueInLastCondition(): iterable { yield [false, [ [ @@ -135,9 +135,9 @@ public function dataReportAlwaysTrueInLastCondition(): iterable } /** - * @dataProvider dataReportAlwaysTrueInLastCondition * @param list $expectedErrors */ + #[DataProvider('dataReportAlwaysTrueInLastCondition')] public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php index 24080f2e00..f3ed1ef002 100644 --- a/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php @@ -20,17 +20,16 @@ protected function getRule(): TRule return new LogicalXorConstantConditionRule( new ConstantConditionRuleHelper( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition, + true, ); } diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionDoNotRememberPossiblyImpureValuesRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionDoNotRememberPossiblyImpureValuesRuleTest.php index b52785c6f5..f81136f8d6 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionDoNotRememberPossiblyImpureValuesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionDoNotRememberPossiblyImpureValuesRuleTest.php @@ -4,8 +4,8 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use function array_merge; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -18,21 +18,15 @@ protected function getRule(): Rule return self::getContainer()->getByType(MatchExpressionRule::class); } + #[RequiresPhp('>= 8.1')] public function testBug9357(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-9357.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug9007(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-9007.php'], []); } diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index b727ba76e2..5224ff9d42 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -4,7 +4,8 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -16,24 +17,18 @@ class MatchExpressionRuleTest extends RuleTestCase private bool $reportAlwaysTrueInLastCondition = false; - private bool $disableUnreachable = false; - protected function getRule(): Rule { return new MatchExpressionRule( new ConstantConditionRuleHelper( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, - true, ), - true, - $this->disableUnreachable, $this->reportAlwaysTrueInLastCondition, $this->treatPhpDocTypesAsCertain, ); @@ -61,41 +56,21 @@ public function testRule(): void 28, $tipText, ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 29, - ], [ 'Match arm comparison between 3 and 3 is always true.', 35, $tipText, ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 36, - ], [ 'Match arm comparison between 1 and 1 is always true.', 40, $tipText, ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 41, - ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 42, - ], [ 'Match arm comparison between 1 and 1 is always true.', 46, $tipText, ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 47, - ], [ 'Match expression does not handle remaining value: 3', 50, @@ -138,20 +113,15 @@ public function testBug4857(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug5454(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->analyse([__DIR__ . '/data/bug-5454.php'], []); } + #[RequiresPhp('>= 8.1')] public function testEnums(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/match-enums.php'], [ [ 'Match expression does not handle remaining values: MatchEnums\Foo::THREE|MatchEnums\Foo::TWO', @@ -170,10 +140,6 @@ public function testEnums(): void 76, 'Remove remaining cases below this one and this error will disappear too.', ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 77, - ], [ 'Match arm comparison between MatchEnums\Foo and MatchEnums\Foo::ONE is always false.', 85, @@ -197,21 +163,15 @@ public function testEnums(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug6394(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-6394.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug6115(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/bug-6115.php'], [ [ 'Match expression does not handle remaining value: 3', @@ -220,71 +180,54 @@ public function testBug6115(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug7095(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/bug-7095.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug7176(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } $this->analyse([__DIR__ . '/data/bug-7176.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug6064(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } $this->analyse([__DIR__ . '/data/bug-6064.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug6647(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } $this->analyse([__DIR__ . '/data/bug-6647.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug7622(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/bug-7622.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug7698(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/bug-7698.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug7746(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7746.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug8240(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8240.php'], [ [ @@ -292,27 +235,17 @@ public function testBug8240(): void 13, 'Remove remaining cases below this one and this error will disappear too.', ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 14, - ], [ '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.', ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 29, - ], ]); } + #[RequiresPhp('>= 8.1')] public function testLastArmAlwaysTrue(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } $this->treatPhpDocTypesAsCertain = true; $tipText = 'Remove remaining cases below this one and this error will disappear too.'; $this->analyse([__DIR__ . '/data/last-match-arm-always-true.php'], [ @@ -321,41 +254,21 @@ public function testLastArmAlwaysTrue(): void 22, $tipText, ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 23, - ], [ 'Match arm comparison between $this(LastMatchArmAlwaysTrue\Foo)&LastMatchArmAlwaysTrue\Foo::TWO and LastMatchArmAlwaysTrue\Foo::TWO is always true.', 31, $tipText, ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 32, - ], [ 'Match arm comparison between $this(LastMatchArmAlwaysTrue\Foo)&LastMatchArmAlwaysTrue\Foo::TWO and LastMatchArmAlwaysTrue\Foo::TWO is always true.', 40, $tipText, ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 41, - ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 42, - ], [ 'Match arm comparison between $this(LastMatchArmAlwaysTrue\Bar)&LastMatchArmAlwaysTrue\Bar::ONE and LastMatchArmAlwaysTrue\Bar::ONE is always true.', 62, $tipText, ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 63, - ], [ 'Match arm comparison between 1 and 0 is always false.', 70, @@ -367,55 +280,9 @@ public function testLastArmAlwaysTrue(): void ]); } - public function dataReportAlwaysTrueInLastCondition(): iterable + public static function dataReportAlwaysTrueInLastCondition(): iterable { - yield [false, false, [ - [ - 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', - 23, - 'Remove remaining cases below this one and this error will disappear too.', - ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 24, - ], - [ - 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', - 49, - 'Remove remaining cases below this one and this error will disappear too.', - ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 50, - ], - ]]; - yield [true, false, [ - [ - 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', - 15, - ], - [ - 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', - 23, - ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 24, - ], - [ - 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', - 45, - ], - [ - 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', - 49, - ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 50, - ], - ]]; - yield [false, true, [ + yield [false, [ [ 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', 23, @@ -427,7 +294,7 @@ public function dataReportAlwaysTrueInLastCondition(): iterable 'Remove remaining cases below this one and this error will disappear too.', ], ]]; - yield [true, true, [ + yield [true, [ [ 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', 15, @@ -448,155 +315,124 @@ public function dataReportAlwaysTrueInLastCondition(): iterable } /** - * @dataProvider dataReportAlwaysTrueInLastCondition * @param list $expectedErrors */ - public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, bool $disableUnreachable, array $expectedErrors): void + #[RequiresPhp('>= 8.1')] + #[DataProvider('dataReportAlwaysTrueInLastCondition')] + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } $this->treatPhpDocTypesAsCertain = true; $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; - $this->disableUnreachable = $disableUnreachable; $this->analyse([__DIR__ . '/data/match-always-true-last-arm.php'], $expectedErrors); } + #[RequiresPhp('>= 8.0')] public function testBug8932(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/bug-8932.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug8937(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/bug-8937.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug8900(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/bug-8900.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug4451(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-4451.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug9007(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-9007.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug9457(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-9457.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug8614(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-8614.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug8536(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-8536.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug9499(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-9499.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug6407(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/bug-6407.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBugUnhandledTrueWithComplexCondition(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-unhandled-true-with-complex-condition.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug11246(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-11246.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug9879(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-9879.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug11313(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-11313.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug9436(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $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, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index ae47818512..f05edf6b3d 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -4,7 +4,8 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -16,7 +17,10 @@ class NumberComparisonOperatorsConstantConditionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new NumberComparisonOperatorsConstantConditionRule($this->treatPhpDocTypesAsCertain); + return new NumberComparisonOperatorsConstantConditionRule( + $this->treatPhpDocTypesAsCertain, + true, + ); } public function testBug8277(): void @@ -102,11 +106,9 @@ public function testBug5295(): void $this->analyse([__DIR__ . '/data/bug-5295.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug7052(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } $this->analyse([__DIR__ . '/data/bug-7052.php'], [ [ 'Comparison operation ">" between Bug7052\Foo::A and Bug7052\Foo::B is always false.', @@ -162,7 +164,7 @@ public function testBug8643(): void $this->analyse([__DIR__ . '/data/bug-8643.php'], []); } - public function dataTreatPhpDocTypesAsCertain(): iterable + public static function dataTreatPhpDocTypesAsCertain(): iterable { yield [ false, @@ -186,9 +188,9 @@ public function dataTreatPhpDocTypesAsCertain(): iterable } /** - * @dataProvider dataTreatPhpDocTypesAsCertain * @param list $expectedErrors */ + #[DataProvider('dataTreatPhpDocTypesAsCertain')] public function testTreatPhpDocTypesAsCertain(bool $treatPhpDocTypesAsCertain, array $expectedErrors): void { $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; @@ -231,4 +233,28 @@ public function testBug6467(): void $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'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index e1c99901de..a0efad16b4 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -2,8 +2,11 @@ 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; @@ -13,15 +16,18 @@ class StrictComparisonOfDifferentTypesRuleTest extends RuleTestCase { - private bool $checkAlwaysTrueStrictComparison; - private bool $reportAlwaysTrueInLastCondition = false; private bool $treatPhpDocTypesAsCertain = true; protected function getRule(): Rule { - return new StrictComparisonOfDifferentTypesRule($this->checkAlwaysTrueStrictComparison, $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition); + return new StrictComparisonOfDifferentTypesRule( + self::getContainer()->getByType(RicherScopeGetTypeHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -31,7 +37,6 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool public function testStrictComparison(): void { - $this->checkAlwaysTrueStrictComparison = true; $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse( [__DIR__ . '/data/strict-comparison.php'], @@ -102,12 +107,12 @@ public function testStrictComparison(): void 140, ], [ - 'Strict comparison using !== between StrictComparison\Foo|null and 1 will always evaluate to true.', - 154, + 'Strict comparison using === between non-empty-array and null will always evaluate to false.', + 150, ], [ - 'Strict comparison using === between non-empty-array and null will always evaluate to false.', - 164, + 'Strict comparison using !== between StrictComparison\Foo|null and 1 will always evaluate to true.', + 161, ], [ 'Strict comparison using !== between StrictComparison\Node|null and false will always evaluate to true.', @@ -148,7 +153,7 @@ public function testStrictComparison(): void 335, ], [ - 'Strict comparison using === between int<0, max> and \'string\' will always evaluate to false.', + 'Strict comparison using === between int<1, max> and \'string\' will always evaluate to false.', 343, ], [ @@ -211,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.', @@ -265,158 +272,25 @@ public function testStrictComparison(): void 996, 'Remove remaining cases below this one and this error will disappear too.', ], - ], - ); - } - - public function testStrictComparisonWithoutAlwaysTrue(): void - { - $this->checkAlwaysTrueStrictComparison = false; - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->analyse( - [__DIR__ . '/data/strict-comparison.php'], - [ [ - '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 lowercase-string|false and \'AB\' will always evaluate to false.', + 1014, $tipText, ], [ - 'Strict comparison using === between true and false will always evaluate to false.', - 30, + '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 false and true will always evaluate to false.', - 31, + '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 1.0 and 1 will always evaluate to false.', - 46, - ], - [ - 'Strict comparison using === between 1 and 1.0 will always evaluate to false.', - 47, - ], - [ - 'Strict comparison using === between string and null will always evaluate to false.', - 69, - ], - [ - 'Strict comparison using === between 1|2|3 and null will always evaluate to false.', - 98, - ], - [ - 'Strict comparison using === between non-empty-array and null will always evaluate to false.', - 140, - ], - [ - 'Strict comparison using === between non-empty-array and null will always evaluate to false.', - 164, - ], - [ - 'Strict comparison using === between 1 and 2 will always evaluate to false.', - 284, - ], - [ - 'Strict comparison using === between array{X: 1} and array{X: 2} will always evaluate to false.', - 292, - ], - [ - 'Strict comparison using === between array{X: 1, Y: 2} and array{X: 2, Y: 1} will always evaluate to false.', - 300, - ], - [ - 'Strict comparison using === between 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<1, max> and \'string\' will always evaluate to false.', - 335, - ], - [ - 'Strict comparison using === between int<0, max> and \'string\' will always evaluate to false.', - 343, - ], - [ - 'Strict comparison using === between int<0, max> and \'string\' will always evaluate to false.', - 360, - ], - [ - 'Strict comparison using === between int<1, max> and \'string\' will always evaluate to false.', - 368, - ], - [ - 'Strict comparison using === between float and \'string\' will always evaluate to false.', - 386, - ], - [ - 'Strict comparison using === between float and \'string\' will always evaluate to false.', - 394, - ], - [ - 'Strict comparison using !== between null and null will always evaluate to false.', - 408, - ], - [ - 'Strict comparison using === between (int|int<2, max>|string) and 1.0 will always evaluate to false.', - 464, - ], - [ - 'Strict comparison using === between (int|int<2, max>|string) and stdClass will always evaluate to false.', - 466, - ], - [ - 'Strict comparison using === between 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<10, max> and \'foo\' will always evaluate to false.', - 635, - ], - [ - 'Strict comparison using === between string|null and 1 will always evaluate to false.', - 685, - ], - [ - 'Strict comparison using === between string|null and 1 will always evaluate to false.', - 695, - ], - [ - 'Strict comparison using === between string|null and 1 will always evaluate to false.', - 705, - ], - [ - 'Strict comparison using === between mixed and \'foo\' will always evaluate to false.', - 808, - ], - [ - 'Strict comparison using === between NAN and NAN will always evaluate to false.', - 980, - ], - [ - 'Strict comparison using !== between INF and INF will always evaluate to false.', - 982, + '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.', ], ], ); @@ -424,7 +298,6 @@ public function testStrictComparisonWithoutAlwaysTrue(): void public function testStrictComparisonPhp71(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/strict-comparison-71.php'], [ [ 'Strict comparison using === between null and null will always evaluate to true.', @@ -435,7 +308,6 @@ public function testStrictComparisonPhp71(): void public function testStrictComparisonPropertyNativeTypesPhp74(): void { - $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.', @@ -458,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.', @@ -479,31 +349,26 @@ public function testBug1860(): void public function testBug3544(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-3544.php'], []); } public function testBug2675(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-2675.php'], []); } public function testBug2220(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-2220.php'], []); } public function testBug1707(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-1707.php'], []); } public function testBug3357(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-3357.php'], []); } @@ -512,7 +377,6 @@ public function testBug4848(): void if (PHP_INT_SIZE !== 8) { $this->markTestSkipped('Test requires 64-bit platform.'); } - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-4848.php'], [ [ 'Strict comparison using === between \'18446744073709551615\' and \'9223372036854775807\' will always evaluate to false.', @@ -523,25 +387,21 @@ public function testBug4848(): void public function testBug4793(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-4793.php'], []); } public function testBug5062(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-5062.php'], []); } public function testBug3366(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-3366.php'], []); } public function testBug5362(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-5362.php'], [ [ 'Strict comparison using === between 0 and 1|2 will always evaluate to false.', @@ -552,8 +412,6 @@ public function testBug5362(): void public function testBug6939(): void { - $this->checkAlwaysTrueStrictComparison = true; - if (PHP_VERSION_ID < 80000) { $this->analyse([__DIR__ . '/data/bug-6939.php'], []); return; @@ -569,13 +427,11 @@ public function testBug6939(): void public function testBug7166(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-7166.php'], []); } public function testBug7555(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-7555.php'], [ [ 'Strict comparison using === between 2 and 2 will always evaluate to true.', @@ -587,30 +443,30 @@ public function testBug7555(): void public function testBug7257(): void { - $this->checkAlwaysTrueStrictComparison = false; $this->analyse([__DIR__ . '/data/bug-7257.php'], []); } public function testBug5474(): void { - $this->checkAlwaysTrueStrictComparison = false; $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->checkAlwaysTrueStrictComparison = false; $this->analyse([__DIR__ . '/data/bug-7684.php'], []); } public function testBug6181(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-6181.php'], []); } @@ -618,7 +474,6 @@ public function testBug2851b(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-2851b.php'], [ [ 'Strict comparison using === between 0 and 0 will always evaluate to true.', @@ -630,17 +485,12 @@ public function testBug2851b(): void public function testBug8158(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-8158.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug8485(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-8485.php'], [ [ 'Strict comparison using === between Bug8485\E::c and Bug8485\E::c will always evaluate to true.', @@ -678,39 +528,27 @@ public function testBug8485(): void public function testBug8516(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-8516.php'], []); } public function testPhpUnitIntegration(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/phpunit-integration.php'], []); } public function testBug8586(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-8586.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug4242(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-4242.php'], []); } public function testBug3633(): void { - $this->checkAlwaysTrueStrictComparison = true; $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse([__DIR__ . '/data/bug-3633.php'], [ [ @@ -781,7 +619,6 @@ public function testBug3633(): void public function testLastConditionAlwaysTrue(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/strict-comparison-last-condition-always-true.php'], [ [ 'Strict comparison using === between \'bar\' and \'bar\' will always evaluate to true.', @@ -793,35 +630,28 @@ public function testLastConditionAlwaysTrue(): void public function testBug3019(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3019.php'], []); } public function testBug7578(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/bug-7578.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug6260(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - - $this->checkAlwaysTrueStrictComparison = true; $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/bug-6260.php'], []); } public function testBug8736(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-8736.php'], []); } - public function dataLastMatchArm(): iterable + public static function dataLastMatchArm(): iterable { yield [false, [ [ @@ -886,29 +716,28 @@ public function dataLastMatchArm(): iterable } /** - * @dataProvider dataLastMatchArm * @param list $expectedErrors */ + #[RequiresPhp('>= 8.1')] + #[DataProvider('dataLastMatchArm')] public function testLastMatchArm(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - - $this->checkAlwaysTrueStrictComparison = true; $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->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-8776-1.php'], []); } public function testBug8776Part2(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-8776-2.php'], []); } @@ -929,13 +758,11 @@ public function testBug5978(): void $expectedErrors = []; } - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-5978.php'], $expectedErrors); } public function testBug9104(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-9104.php'], [ [ 'Strict comparison using === between int<1, max> and 0 will always evaluate to false.', @@ -945,13 +772,9 @@ public function testBug9104(): void ]); } + #[RequiresPhp('>= 8.1')] public function testEnumTips(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/strict-comparison-enum-tips.php'], [ [ 'Strict comparison using === between StrictComparisonEnumTips\SomeEnum::Two and StrictComparisonEnumTips\SomeEnum::Two will always evaluate to true.', @@ -961,13 +784,9 @@ public function testEnumTips(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug9142(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-9142.php'], [ [ 'Strict comparison using === between $this(Bug9142\MyEnum) and Bug9142\MyEnum::Three will always evaluate to false.', @@ -980,51 +799,36 @@ public function testBug9142(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug4061(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-4061.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug9723(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-9723.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug9723b(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-9723b.php'], []); } public function testBug8366(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8366.php'], []); } public function testBug3300(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/../../Analyser/data/bug-3300.php'], []); } public function testBug11035(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-11035.php'], [ [ "Strict comparison using === between '0' and non-falsy-string will always evaluate to false.", @@ -1036,26 +840,170 @@ public function testBug11035(): void public function testBug9804(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-9804.php'], []); } public function testBug11161(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-11161.php'], []); } public function testBug10697(): void { - $this->checkAlwaysTrueStrictComparison = true; $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->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-10493.php'], []); } + public function testBug7173(): void + { + $this->analyse([__DIR__ . '/data/bug-7173.php'], []); + } + + public function testHashing(): void + { + $this->analyse([__DIR__ . '/data/hashing.php'], [ + [ + "Strict comparison using === between lowercase-string&non-falsy-string and 'ABC' will always evaluate to false.", + 9, + ], + [ + "Strict comparison using === between (lowercase-string&non-falsy-string)|false and 'ABC' will always evaluate to false.", + 12, + ], + [ + "Strict comparison using === between (lowercase-string&non-falsy-string)|(non-falsy-string&numeric-string) and 'A' will always evaluate to false.", + 31, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + + public function testBug12772(): void + { + $this->analyse([__DIR__ . '/data/bug-12772.php'], []); + } + + public function testBug12748(): void + { + $this->analyse([__DIR__ . '/data/bug-12748.php'], []); + } + + public function testBug11019(): void + { + $this->analyse([__DIR__ . '/data/bug-11019.php'], []); + } + + public function testBug12946(): void + { + $this->analyse([__DIR__ . '/data/bug-12946.php'], []); + } + + public function testBug10884(): void + { + $this->analyse([__DIR__ . '/data/bug-10884.php'], []); + } + + public function testBug13208(): void + { + $this->analyse([__DIR__ . '/data/bug-13208.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php index 71fce9241d..250f639068 100644 --- a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php @@ -18,16 +18,15 @@ protected function getRule(): Rule return new TernaryOperatorConstantConditionRule( new ConstantConditionRuleHelper( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, + true, ); } diff --git a/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php b/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php deleted file mode 100644 index 7843308281..0000000000 --- a/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php +++ /dev/null @@ -1,139 +0,0 @@ - - */ -class UnreachableIfBranchesRuleTest extends RuleTestCase -{ - - private bool $treatPhpDocTypesAsCertain; - - protected function getRule(): Rule - { - return new UnreachableIfBranchesRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), - $this->getTypeSpecifier(), - [], - $this->treatPhpDocTypesAsCertain, - true, - ), - $this->treatPhpDocTypesAsCertain, - true, - ), - $this->treatPhpDocTypesAsCertain, - false, - ); - } - - 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, - ], - ]); - } - - 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 testBug7491(): void - { - $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/bug-7491.php'], []); - } - -} diff --git a/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php b/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php deleted file mode 100644 index c145cc7cfa..0000000000 --- a/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php +++ /dev/null @@ -1,111 +0,0 @@ - - */ -class UnreachableTernaryElseBranchRuleTest extends RuleTestCase -{ - - private bool $treatPhpDocTypesAsCertain; - - protected function getRule(): Rule - { - return new UnreachableTernaryElseBranchRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), - $this->getTypeSpecifier(), - [], - $this->treatPhpDocTypesAsCertain, - true, - ), - $this->treatPhpDocTypesAsCertain, - true, - ), - $this->treatPhpDocTypesAsCertain, - false, - ); - } - - 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, - ], - [ - 'Else branch is unreachable because ternary operator condition is always true.', - 20, - ], - ]); - } - - 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, - ], - ]); - } - - public function testBug3019(): void - { - $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3019.php'], []); - } - - public function testBug7686(): void - { - $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/bug-7686.php'], []); - } - -} diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php index 42675f9d8a..a41bc8158f 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php @@ -18,16 +18,15 @@ protected function getRule(): Rule return new WhileLoopAlwaysFalseConditionRule( new ConstantConditionRuleHelper( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, + true, ); } diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php index e6f158f91a..7a1e99f5c1 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php @@ -18,16 +18,15 @@ protected function getRule(): Rule return new WhileLoopAlwaysTrueConditionRule( new ConstantConditionRuleHelper( new ImpossibleCheckTypeHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, - true, ), $this->treatPhpDocTypesAsCertain, + true, ); } diff --git a/tests/PHPStan/Rules/Comparison/data/TestTypeOverwriteSpecifyingExtensions.php b/tests/PHPStan/Rules/Comparison/data/TestTypeOverwriteSpecifyingExtensions.php index 4e2acbd198..1ba7c4855f 100644 --- a/tests/PHPStan/Rules/Comparison/data/TestTypeOverwriteSpecifyingExtensions.php +++ b/tests/PHPStan/Rules/Comparison/data/TestTypeOverwriteSpecifyingExtensions.php @@ -51,8 +51,8 @@ public function specifyTypes( $node->var, $newType, TypeSpecifierContext::createTruthy(), - true - ); + $scope, + )->setAlwaysOverwriteTypes(); } } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-10884.php b/tests/PHPStan/Rules/Comparison/data/bug-10884.php new file mode 100644 index 0000000000..36845f3f47 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-10884.php @@ -0,0 +1,49 @@ + $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-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 @@ + + */ +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-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-13291.php b/tests/PHPStan/Rules/Comparison/data/bug-13291.php new file mode 100644 index 0000000000..6b9e31a4fd --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13291.php @@ -0,0 +1,10 @@ += 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-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 @@ +&hasOffsetValue(\'bsw\', string)', $result); + assertType('non-empty-array&hasOffsetValue(\'bsw\', string)', $result); $result['bsw'] = (int) $result['bsw']; assertType("non-empty-array&hasOffsetValue('bsw', int)", $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 @@ += 7.4 += 7.4 += 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-7173.php b/tests/PHPStan/Rules/Comparison/data/bug-7173.php new file mode 100644 index 0000000000..039b2bd667 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7173.php @@ -0,0 +1,19 @@ + 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-7686.php b/tests/PHPStan/Rules/Comparison/data/bug-7686.php deleted file mode 100644 index be05325779..0000000000 --- a/tests/PHPStan/Rules/Comparison/data/bug-7686.php +++ /dev/null @@ -1,25 +0,0 @@ - $input - * @return array<'return'|int, string> - */ - public static function test(array $input): array - { - $output = []; - foreach($input as $match) { - if (array_key_exists($match['name'], $output) == false) { - $output[$match['name']] = ''; - } - if (($match['type'] === '') || (in_array($match['type'], explode('|', $output[$match['name']]), true) === true)) { - continue; - } - $output[$match['name']] = ($output[$match['name']] === '' ? $match['type'] : $output[$match['name']] . '|' . $match['type']); - } - return $output; - } -} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7937.php b/tests/PHPStan/Rules/Comparison/data/bug-7937.php new file mode 100644 index 0000000000..d82bc1e465 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7937.php @@ -0,0 +1,31 @@ += 8.0 + +declare(strict_types = 1); namespace Bug8169; 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 index d6510c8ad6..5412e7a695 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-8474.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-8474.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 + + */ +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-8727.php b/tests/PHPStan/Rules/Comparison/data/bug-8727.php index 85da7f5791..dad24f9c7d 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-8727.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-8727.php @@ -1,4 +1,4 @@ -= 7.4 + $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-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-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-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-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-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.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/strict-comparison-last-condition-always-true.php b/tests/PHPStan/Rules/Comparison/data/strict-comparison-last-condition-always-true.php index 201c9ce0b2..728f3ed2e8 100644 --- 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 @@ -1,4 +1,4 @@ -= 7.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 @@ -1002,3 +1002,52 @@ public function doFoo() } } + +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/uppercase-string.php b/tests/PHPStan/Rules/Comparison/data/uppercase-string.php new file mode 100644 index 0000000000..fbd7a3eaf7 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/uppercase-string.php @@ -0,0 +1,31 @@ + + */ +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 7b6d03ce19..0da9819e08 100644 --- a/tests/PHPStan/Rules/Constants/ConstantRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ConstantRuleTest.php @@ -14,7 +14,12 @@ class ConstantRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ConstantRule(); + return new ConstantRule(true); + } + + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; } public function testConstants(): void @@ -80,4 +85,45 @@ public function testDefinedScopeMerge(): void ]); } + public function testRememberedConstructorScope(): void + { + $this->analyse([__DIR__ . '/data/remembered-constructor-scope.php'], [ + [ + 'Constant REMEMBERED_FOO not found.', + 23, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Constant REMEMBERED_FOO not found.', + 38, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Constant REMEMBERED_FOO not found.', + 51, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Constant REMEMBERED_FOO not found.', + 65, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Constant XYZ22 not found.', + 87, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Constant XYZ not found.', + 88, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Constant XYZ33 not found.', + 98, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Constants/DynamicClassConstantFetchRuleTest.php b/tests/PHPStan/Rules/Constants/DynamicClassConstantFetchRuleTest.php index 2806935226..5d829849db 100644 --- a/tests/PHPStan/Rules/Constants/DynamicClassConstantFetchRuleTest.php +++ b/tests/PHPStan/Rules/Constants/DynamicClassConstantFetchRuleTest.php @@ -18,7 +18,7 @@ protected function getRule(): TRule { return new DynamicClassConstantFetchRule( self::getContainer()->getByType(PhpVersion::class), - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false), + new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true), ); } diff --git a/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php b/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php index 1fac7d6798..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 @@ -19,7 +20,7 @@ protected function getRule(): Rule return new FinalConstantRule(new PhpVersion($this->phpVersionId)); } - public function dataRule(): array + public static function dataRule(): array { return [ [ @@ -39,9 +40,9 @@ public function dataRule(): array } /** - * @dataProvider dataRule * @param list $errors */ + #[DataProvider('dataRule')] public function testRule(int $phpVersionId, array $errors): void { $this->phpVersionId = $phpVersionId; 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/MissingClassConstantTypehintRuleTest.php b/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php index 89b6302e8a..2c95dbb317 100644 --- a/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php @@ -5,7 +5,7 @@ use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -15,7 +15,7 @@ class MissingClassConstantTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - return new MissingClassConstantTypehintRule(new MissingTypehintCheck(true, true, true, true, [])); + return new MissingClassConstantTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -37,20 +37,15 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.2')] public function testBug8957(): void { - if (PHP_VERSION_ID < 80200) { - $this->markTestSkipped('This test needs PHP 8.2'); - } $this->analyse([__DIR__ . '/data/bug-8957.php'], []); } + #[RequiresPhp('>= 8.3')] public function testRuleShouldNotApplyToNativeTypes(): void { - if (PHP_VERSION_ID < 80300) { - $this->markTestSkipped('This test needs PHP 8.3'); - } - $this->analyse([__DIR__ . '/data/class-constant-native-type.php'], [ [ 'Constant ClassConstantNativeTypeForMissingTypehintRule\Foo::B type has no value type specified in iterable type array.', diff --git a/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php b/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php index 6ce51b0297..5e14d98461 100644 --- a/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php +++ b/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use const PHP_VERSION_ID; /** @extends RuleTestCase */ @@ -90,12 +91,9 @@ public function testFinal(): void $this->analyse([__DIR__ . '/data/overriding-final-constant.php'], $errors); } + #[RequiresPhp('>= 8.3')] public function testNativeTypes(): void { - if (PHP_VERSION_ID < 80300) { - $this->markTestSkipped('Test requires PHP 8.3.'); - } - $this->analyse([__DIR__ . '/data/overriding-constant-native-types.php'], [ [ 'Native type int|string of constant OverridingConstantNativeTypes\Bar::D is not covariant with native type int of constant OverridingConstantNativeTypes\Foo::D.', diff --git a/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php index 7942506ca1..5405390a90 100644 --- a/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule as TRule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -60,12 +60,9 @@ public function testBug5655(): void $this->analyse([__DIR__ . '/data/bug-5655.php'], []); } + #[RequiresPhp('>= 8.3')] public function testNativeType(): void { - if (PHP_VERSION_ID < 80300) { - $this->markTestSkipped('Test requires PHP 8.3.'); - } - $this->analyse([__DIR__ . '/data/value-assigned-to-class-constant-native-type.php'], [ [ 'Constant ValueAssignedToClassConstantNativeType\Foo::BAR (int) does not accept value \'bar\'.', @@ -86,12 +83,9 @@ public function testNativeType(): void ]); } + #[RequiresPhp('>= 8.3')] public function testBug10212(): void { - if (PHP_VERSION_ID < 80300) { - $this->markTestSkipped('Test requires PHP 8.3.'); - } - $this->analyse([__DIR__ . '/data/bug-10212.php'], [ [ 'Constant Bug10212\HelloWorld::B (Bug10212\X\Foo) does not accept value Bug10212\Foo::Bar.', 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 @@ + - */ -class BetterNoopRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new BetterNoopRule(new ExprPrinter(new Printer())); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/noop.php'], [ - [ - 'Expression "$arr" on a separate line does not do anything.', - 9, - ], - [ - 'Expression "$arr[\'test\']" on a separate line does not do anything.', - 10, - ], - [ - 'Expression "$foo::$test" on a separate line does not do anything.', - 11, - ], - [ - 'Expression "$foo->test" on a separate line does not do anything.', - 12, - ], - [ - 'Expression "\'foo\'" on a separate line does not do anything.', - 14, - ], - [ - 'Expression "1" on a separate line does not do anything.', - 15, - ], - [ - 'Expression "@\'foo\'" on a separate line does not do anything.', - 17, - ], - [ - 'Expression "+1" on a separate line does not do anything.', - 18, - ], - [ - 'Expression "-1" on a separate line does not do anything.', - 19, - ], - [ - 'Expression "isset($test)" on a separate line does not do anything.', - 25, - ], - [ - 'Expression "empty($test)" on a separate line does not do anything.', - 26, - ], - [ - 'Expression "true" on a separate line does not do anything.', - 27, - ], - [ - 'Expression "\DeadCodeNoop\Foo::TEST" on a separate line does not do anything.', - 28, - ], - [ - 'Expression "(string) 1" on a separate line does not do anything.', - 30, - ], - [ - 'Unused result of "xor" operator.', - 32, - 'This operator has unexpected precedence, try disambiguating the logic with parentheses ().', - ], - [ - 'Unused result of "and" operator.', - 35, - 'This operator has unexpected precedence, try disambiguating the logic with parentheses ().', - ], - [ - 'Unused result of "or" operator.', - 38, - 'This operator has unexpected precedence, try disambiguating the logic with parentheses ().', - ], - [ - 'Unused result of ternary operator.', - 40, - ], - [ - 'Unused result of ternary operator.', - 41, - ], - [ - 'Unused result of "||" operator.', - 46, - ], - [ - 'Unused result of "&&" operator.', - 49, - ], - ]); - } - - public function testNullsafe(): void - { - $this->analyse([__DIR__ . '/data/nullsafe-property-fetch-noop.php'], [ - [ - 'Expression "$ref?->name" on a separate line does not do anything.', - 10, - ], - ]); - } - - public function testRuleImpurePoints(): void - { - $this->analyse([__DIR__ . '/data/noop-impure-points.php'], [ - [ - 'Unused result of "&&" operator.', - 12, - ], - [ - 'Expression "$b()" on a separate line does not do anything.', - 59, - ], - [ - 'Expression "new class…" on a separate line does not do anything.', - 98, - ], - [ - 'Expression "new class…" on a separate line does not do anything.', - 104, - ], - ]); - } - - public function testBug11001(): void - { - if (PHP_VERSION_ID < 70400) { - self::markTestSkipped('Test requires PHP 7.4.'); - } - - $this->analyse([__DIR__ . '/data/bug-11001.php'], []); - } - - public function testBug11361(): void - { - $this->analyse([__DIR__ . '/data/bug-11361.php'], []); - } - -} diff --git a/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php index ac630c4880..c4dd2583f3 100644 --- a/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php @@ -29,7 +29,7 @@ public function testRule(): void protected function getCollectors(): array { return [ - new PossiblyPureNewCollector($this->createReflectionProvider()), + new PossiblyPureNewCollector(self::createReflectionProvider()), new ConstructorWithoutImpurePointsCollector(), ]; } diff --git a/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php index 3c5fe7a2b9..6fc162edc6 100644 --- a/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php @@ -29,7 +29,7 @@ public function testRule(): void protected function getCollectors(): array { return [ - new PossiblyPureFuncCallCollector($this->createReflectionProvider()), + new PossiblyPureFuncCallCollector(self::createReflectionProvider()), new FunctionWithoutImpurePointsCollector(), ]; } diff --git a/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php index 77feb89d7d..e0bd3d2168 100644 --- a/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -28,10 +28,22 @@ public function testRule(): void '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, @@ -48,22 +60,24 @@ public function testRule(): void '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.', - 119, + 139, ], [ 'Call to method CallToMethodWithoutImpurePoints\CallsPrivateMethodWithoutImpurePoints::doBar() on a separate line has no effect.', - 127, + 147, ], ]); } + #[RequiresPhp('>= 8.0')] public function testBug11011(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->analyse([__DIR__ . '/data/bug-11011.php'], [ [ 'Call to method Bug11011\AnotherPureImpl::doFoo() on a separate line has no effect.', @@ -72,6 +86,12 @@ public function testBug11011(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testBug12379(): void + { + $this->analyse([__DIR__ . '/data/bug-12379.php'], []); + } + protected function getCollectors(): array { return [ diff --git a/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php b/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php index edffaa5b9a..5750c03c3e 100644 --- a/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php @@ -15,7 +15,7 @@ class NoopRuleTest extends RuleTestCase protected function getRule(): Rule { - return new NoopRule(new ExprPrinter(new Printer()), false); + return new NoopRule(new ExprPrinter(new Printer())); } public function testRule(): void @@ -77,6 +77,37 @@ public function testRule(): void 'Expression "(string) 1" on a separate line does not do anything.', 30, ], + [ + 'Unused result of "xor" operator.', + 32, + 'This operator has unexpected precedence, try disambiguating the logic with parentheses ().', + ], + [ + 'Unused result of "and" operator.', + 35, + 'This operator has unexpected precedence, try disambiguating the logic with parentheses ().', + ], + [ + 'Unused result of "or" operator.', + 38, + 'This operator has unexpected precedence, try disambiguating the logic with parentheses ().', + ], + [ + 'Unused result of ternary operator.', + 40, + ], + [ + 'Unused result of ternary operator.', + 41, + ], + [ + 'Unused result of "||" operator.', + 46, + ], + [ + 'Unused result of "&&" operator.', + 49, + ], ]); } @@ -90,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 191ba39134..e79a185935 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -69,7 +71,7 @@ public function testRuleTopLevel(): void ]); } - public function dataBugWithoutGitHubIssue1(): array + public static function dataBugWithoutGitHubIssue1(): array { return [ [ @@ -81,9 +83,7 @@ public function dataBugWithoutGitHubIssue1(): array ]; } - /** - * @dataProvider dataBugWithoutGitHubIssue1 - */ + #[DataProvider('dataBugWithoutGitHubIssue1')] public function testBugWithoutGitHubIssue1(bool $treatPhpDocTypesAsCertain): void { $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; @@ -224,4 +224,153 @@ public function testBug11179(): void $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 24bde42de8..79e15a843c 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php @@ -2,13 +2,13 @@ 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; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -22,7 +22,7 @@ protected function getRule(): Rule new DirectAlwaysUsedClassConstantsExtensionProvider([ new class() implements AlwaysUsedClassConstantsExtension { - public function isAlwaysUsed(ConstantReflection $constant): bool + public function isAlwaysUsed(ClassConstantReflection $constant): bool { return $constant->getDeclaringClass()->getName() === TestExtension::class && $constant->getName() === 'USED'; @@ -54,12 +54,9 @@ public function testBug5651(): void $this->analyse([__DIR__ . '/data/bug-5651.php'], []); } + #[RequiresPhp('>= 8.1')] public function testEnums(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs PHP 8.1'); - } - $this->analyse([__DIR__ . '/data/unused-private-constant-enum.php'], [ [ 'Constant UnusedPrivateConstantEnum\Foo::TEST_2 is unused.', @@ -74,21 +71,15 @@ public function testBug6758(): void $this->analyse([__DIR__ . '/data/bug-6758.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug8204(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/bug-8204.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug9005(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-9005.php'], []); } @@ -97,12 +88,9 @@ public function testBug9765(): void $this->analyse([__DIR__ . '/data/bug-9765.php'], []); } + #[RequiresPhp('>= 8.3')] public function testDynamicConstantFetch(): void { - if (PHP_VERSION_ID < 80300) { - $this->markTestSkipped('Test requires PHP 8.3.'); - } - $this->analyse([__DIR__ . '/data/unused-private-constant-dynamic-fetch.php'], [ [ 'Constant UnusedPrivateConstantDynamicFetch\Baz::FOO is unused.', diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php index 246237f97f..093d372358 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php @@ -7,7 +7,7 @@ use PHPStan\Rules\Methods\DirectAlwaysUsedMethodExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -72,21 +72,15 @@ public function testNullsafe(): void $this->analyse([__DIR__ . '/data/nullsafe-unused-private-method.php'], []); } + #[RequiresPhp('>= 8.1')] public function testFirstClassCallable(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/callable-unused-private-method.php'], []); } + #[RequiresPhp('>= 8.1')] public function testEnums(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs PHP 8.1'); - } - $this->analyse([__DIR__ . '/data/unused-private-method-enum.php'], [ [ 'Method UnusedPrivateMethodEnunm\Foo::doBaz() is unused.', @@ -114,12 +108,9 @@ public function testBug8346(): void $this->analyse([__DIR__ . '/data/bug-8346.php'], []); } + #[RequiresPhp('>= 8.1')] public function testFalsePositiveWithTraitUse(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs PHP 8.1'); - } - $this->analyse([__DIR__ . '/data/unused-method-false-positive-with-trait.php'], []); } @@ -133,4 +124,14 @@ 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 e17c06a69d..3b909661e8 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php @@ -7,8 +7,8 @@ use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use function in_array; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -22,6 +22,8 @@ class UnusedPrivatePropertyRuleTest extends RuleTestCase /** @var string[] */ private array $alwaysReadTags; + private bool $checkUninitializedProperties = false; + protected function getRule(): Rule { return new UnusedPrivatePropertyRule( @@ -55,7 +57,7 @@ public function isInitialized(PropertyReflection $property, string $propertyName ]), $this->alwaysWrittenTags, $this->alwaysReadTags, - true, + $this->checkUninitializedProperties, ); } @@ -63,6 +65,7 @@ public function testRule(): void { $this->alwaysWrittenTags = []; $this->alwaysReadTags = []; + $this->checkUninitializedProperties = true; $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; @@ -236,15 +239,12 @@ public function testBug5337(): void { $this->alwaysWrittenTags = []; $this->alwaysReadTags = []; + $this->checkUninitializedProperties = true; $this->analyse([__DIR__ . '/data/bug-5337.php'], []); } public function testBug5971(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->alwaysWrittenTags = []; $this->alwaysReadTags = []; $this->analyse([__DIR__ . '/data/bug-5971.php'], []); @@ -252,21 +252,14 @@ public function testBug5971(): void public function testBug6107(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->alwaysWrittenTags = []; $this->alwaysReadTags = []; $this->analyse([__DIR__ . '/data/bug-6107.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug8204(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->alwaysWrittenTags = []; $this->alwaysReadTags = []; $this->analyse([__DIR__ . '/data/bug-8204.php'], []); @@ -293,26 +286,120 @@ public function testBug9765(): void $this->analyse([__DIR__ . '/data/bug-9765.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug10059(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->alwaysWrittenTags = []; $this->alwaysReadTags = []; $this->analyse([__DIR__ . '/data/bug-10059.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug10628(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->alwaysWrittenTags = []; $this->alwaysReadTags = []; $this->analyse([__DIR__ . '/data/bug-10628.php'], []); } + public function testBug8781(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-8781.php'], []); + } + + public function testBug9361(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-9361.php'], []); + } + + public function testBug7251(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-7251.php'], []); + } + + public function testBug11802(): void + { + $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; + + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-11802.php'], [ + [ + 'Property Bug11802\HelloWorld::$isFinal is never read, only written.', + 8, + $tip, + ], + ]); + } + + #[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, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-11001.php b/tests/PHPStan/Rules/DeadCode/data/bug-11001.php index 5351fc8302..4b39b689c1 100644 --- a/tests/PHPStan/Rules/DeadCode/data/bug-11001.php +++ b/tests/PHPStan/Rules/DeadCode/data/bug-11001.php @@ -1,4 +1,4 @@ -= 7.4 += 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 += 7.4 += 7.4 += 7.4 +setToOne($this->bar); + } + + private function setToOne(&$var) + { + $var = 1; + } +} 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-8620.php b/tests/PHPStan/Rules/DeadCode/data/bug-8620.php index cad6d811c2..44bc78cd45 100644 --- a/tests/PHPStan/Rules/DeadCode/data/bug-8620.php +++ b/tests/PHPStan/Rules/DeadCode/data/bug-8620.php @@ -9,7 +9,7 @@ class HelloWorld public function nullCoalesceAndConcatenation (?int $a = null): int { $key = ($a ?? "x") . "-"; - assertType('non-falsy-string', $key); + assertType('lowercase-string&non-falsy-string', $key); if ($key === "x-") { return 0; } return 1; diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-8781.php b/tests/PHPStan/Rules/DeadCode/data/bug-8781.php new file mode 100644 index 0000000000..3e89361028 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-8781.php @@ -0,0 +1,37 @@ + + */ + 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-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/call-to-method-without-impure-points.php b/tests/PHPStan/Rules/DeadCode/data/call-to-method-without-impure-points.php index e9f82b1476..b371026745 100644 --- a/tests/PHPStan/Rules/DeadCode/data/call-to-method-without-impure-points.php +++ b/tests/PHPStan/Rules/DeadCode/data/call-to-method-without-impure-points.php @@ -41,6 +41,26 @@ function (): void { $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() 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 @@ += 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/unused-private-property.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php index e5b8841eb9..97aa923a37 100644 --- a/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php @@ -1,4 +1,4 @@ -= 7.4 + + */ +class DebugScopeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DebugScopeRule(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 7a2ff3aa6a..fb4c489dea 100644 --- a/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php +++ b/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php @@ -13,7 +13,7 @@ class DumpTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new DumpTypeRule($this->createReflectionProvider()); + return new DumpTypeRule(self::createReflectionProvider()); } public function testRuleInPhpStanNamespace(): void 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/debug-scope.php b/tests/PHPStan/Rules/Debug/data/debug-scope.php new file mode 100644 index 0000000000..0e7b8663aa --- /dev/null +++ b/tests/PHPStan/Rules/Debug/data/debug-scope.php @@ -0,0 +1,17 @@ + '']); +dumpPhpDocType(["\0" => 'NUL', 'NUL' => "\0"]); +dumpPhpDocType(["\x01" => 'SOH', 'SOH' => "\x01"]); +dumpPhpDocType(["\t" => 'HT', 'HT' => "\t"]); + +// Space +dumpPhpDocType([" " => 'SP', 'SP' => ' ']); +dumpPhpDocType(["foo " => 'ends with SP', " foo" => 'starts with SP', " foo " => 'surrounded by SP', 'foo' => 'no SP']); + +// Punctuation marks +dumpPhpDocType(["foo?" => 'foo?']); +dumpPhpDocType(["shallwedance" => 'yes']); +dumpPhpDocType(["shallwedance?" => 'yes']); +dumpPhpDocType(["Shall we dance" => 'yes']); +dumpPhpDocType(["Shall we dance?" => 'yes']); +dumpPhpDocType(["shall_we_dance" => 'yes']); +dumpPhpDocType(["shall_we_dance?" => 'yes']); +dumpPhpDocType(["shall-we-dance" => 'yes']); +dumpPhpDocType(["shall-we-dance?" => 'yes']); +dumpPhpDocType(['Let\'s go' => "Let's go"]); +dumpPhpDocType(['Foo\\Bar' => 'Foo\\Bar']); +dumpPhpDocType(['3.14' => 3.14]); +dumpPhpDocType([true => true, false => false]); + +/** + * @template T + * @param T $value + * @return T + */ +function id($value) +{ + dumpPhpDocType($value); + + return $value; +} diff --git a/tests/PHPStan/Rules/Debug/data/file-asserts.php b/tests/PHPStan/Rules/Debug/data/file-asserts.php index abd8e54d07..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); } /** @@ -26,6 +30,7 @@ public function doBar(array $a): void { 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/EnumCases/EnumCaseAttributesRuleTest.php b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php index 26643e506c..1028c66778 100644 --- a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php +++ b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\EnumCases; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -14,6 +13,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -23,31 +23,32 @@ class EnumCaseAttributesRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new EnumCaseAttributesRule( new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), - new PhpVersion(80100), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, 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'], [ diff --git a/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php b/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php index 08f4885067..8dd7f30a05 100644 --- a/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php +++ b/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use function array_merge; /** @@ -15,7 +16,7 @@ class AbilityToDisableImplicitThrowsTest extends RuleTestCase protected function getRule(): Rule { return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( - $this->createReflectionProvider(), + self::createReflectionProvider(), [], [], [], @@ -33,6 +34,41 @@ public function testRule(): void ]); } + #[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( 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 e6c4a38a42..9049c7ad16 100644 --- a/tests/PHPStan/Rules/Exceptions/Bug5364Test.php +++ b/tests/PHPStan/Rules/Exceptions/Bug5364Test.php @@ -15,7 +15,7 @@ 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 index 6b32128600..421fdb719d 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php @@ -14,7 +14,7 @@ class CatchWithUnthrownExceptionRuleStubsTest extends RuleTestCase protected function getRule(): Rule { return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( - $this->createReflectionProvider(), + self::createReflectionProvider(), [], [], [], diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 0958befd2f..272c010d97 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -6,7 +6,7 @@ use InvalidArgumentException; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -22,7 +22,7 @@ class CatchWithUnthrownExceptionRuleTest extends RuleTestCase protected function getRule(): Rule { return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( - $this->createReflectionProvider(), + self::createReflectionProvider(), [], $this->uncheckedExceptionClasses, [], @@ -299,21 +299,14 @@ public function testBug4863(): void $this->analyse([__DIR__ . '/data/bug-4863.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug5866(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->analyse([__DIR__ . '/data/bug-5866.php'], []); } public function testBug4814(): void { - if (PHP_VERSION_ID < 70300) { - $this->markTestSkipped('Test requires PHP 7.3.'); - } - $this->analyse([__DIR__ . '/data/bug-4814.php'], [ [ 'Dead catch - JsonException is never thrown in the try block.', @@ -367,11 +360,15 @@ public function testBug4852(): void $this->analyse([__DIR__ . '/data/bug-4852.php'], [ [ 'Dead catch - Exception is never thrown in the try block.', - 70, + 63, ], [ 'Dead catch - Exception is never thrown in the try block.', - 77, + 78, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 85, ], ]); } @@ -390,12 +387,9 @@ public function testBug5903(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug6115(): void { - if (PHP_VERSION_ID < 80000) { - self::markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/bug-6115.php'], [ [ 'Dead catch - UnhandledMatchError is never thrown in the try block.', @@ -415,10 +409,6 @@ public function testBug6262(): void public function testBug6256(): void { - if (PHP_VERSION_ID < 70400) { - self::markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/bug-6256.php'], [ [ 'Dead catch - TypeError is never thrown in the try block.', @@ -449,10 +439,6 @@ public function testBug6256(): void public function testBug6791(): void { - if (PHP_VERSION_ID < 70400) { - self::markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/bug-6791.php'], [ [ 'Dead catch - TypeError is never thrown in the try block.', @@ -471,19 +457,12 @@ public function testBug6791(): void public function testBug6786(): void { - if (PHP_VERSION_ID < 70400) { - self::markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/bug-6786.php'], []); } + #[RequiresPhp('>= 8.0')] public function testUnionTypeError(): void { - if (PHP_VERSION_ID < 80000) { - self::markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/union-type-error.php'], [ [ 'Dead catch - TypeError is never thrown in the try block.', @@ -619,13 +598,49 @@ public function testBug5650(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug9568(): void { - if (PHP_VERSION_ID < 80000) { - self::markTestSkipped('Test requires PHP 8.0.'); - } - $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/CatchWithUnthrownExceptionRuleWithDisabledMultiCatchTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleWithDisabledMultiCatchTest.php deleted file mode 100644 index debb459f03..0000000000 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleWithDisabledMultiCatchTest.php +++ /dev/null @@ -1,48 +0,0 @@ - - */ -class CatchWithUnthrownExceptionRuleWithDisabledMultiCatchTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( - $this->createReflectionProvider(), - [], - [], - [], - [], - ), true); - } - - public static function getAdditionalConfigFiles(): array - { - return array_merge( - parent::getAdditionalConfigFiles(), - [__DIR__ . '/disable-detect-multi-catch.neon'], - ); - } - - public function testMultiCatchBackwardCompatible(): void - { - $this->analyse([__DIR__ . '/data/unthrown-exception-multi.php'], [ - [ - 'Dead catch - InvalidArgumentException is already caught above.', - 145, - ], - [ - 'Dead catch - InvalidArgumentException is already caught above.', - 156, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php b/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php index 36346ad255..c56c830bb6 100644 --- a/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php @@ -16,14 +16,17 @@ class CaughtExceptionExistenceRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new CaughtExceptionExistenceRule( $reflectionProvider, new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), true, + true, ); } diff --git a/tests/PHPStan/Rules/Exceptions/DefaultExceptionTypeResolverTest.php b/tests/PHPStan/Rules/Exceptions/DefaultExceptionTypeResolverTest.php index 0708028924..1ca947f5d6 100644 --- a/tests/PHPStan/Rules/Exceptions/DefaultExceptionTypeResolverTest.php +++ b/tests/PHPStan/Rules/Exceptions/DefaultExceptionTypeResolverTest.php @@ -8,11 +8,12 @@ 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 [ [ @@ -127,12 +128,12 @@ public function dataIsCheckedException(): array } /** - * @dataProvider dataIsCheckedException * @param string[] $uncheckedExceptionRegexes * @param string[] $uncheckedExceptionClasses * @param string[] $checkedExceptionRegexes * @param string[] $checkedExceptionClasses */ + #[DataProvider('dataIsCheckedException')] public function testIsCheckedException( array $uncheckedExceptionRegexes, array $uncheckedExceptionClasses, @@ -142,7 +143,7 @@ public function testIsCheckedException( 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 716e7b8687..5c0d007b84 100644 --- a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRuleTest.php @@ -16,7 +16,7 @@ protected function getRule(): Rule { return new MissingCheckedExceptionInFunctionThrowsRule( new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver( - $this->createReflectionProvider(), + self::createReflectionProvider(), [], [ShouldNotHappenException::class], [], diff --git a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php index ba61e2900a..aee538052e 100644 --- a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php @@ -18,7 +18,7 @@ protected function getRule(): Rule { return new MissingCheckedExceptionInMethodThrowsRule( new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver( - $this->createReflectionProvider(), + self::createReflectionProvider(), [], [ShouldNotHappenException::class], [], 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 index 9c8181f4a3..55cf2f2628 100644 --- a/tests/PHPStan/Rules/Exceptions/NoncapturingCatchRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/NoncapturingCatchRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -12,14 +14,12 @@ class NoncapturingCatchRuleTest extends RuleTestCase { - private PhpVersion $phpVersion; - protected function getRule(): Rule { - return new NoncapturingCatchRule($this->phpVersion); + return new NoncapturingCatchRule(); } - public function dataRule(): array + public static function dataRule(): array { return [ [ @@ -43,13 +43,19 @@ public function dataRule(): array } /** - * @dataProvider dataRule - * * @param list $expectedErrors */ + #[DataProvider('dataRule')] public function testRule(int $phpVersion, array $expectedErrors): void { - $this->phpVersion = new PhpVersion($phpVersion); + $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', diff --git a/tests/PHPStan/Rules/Exceptions/ThrowExprTypeRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowExprTypeRuleTest.php index 8ef6277fe1..a415096826 100644 --- a/tests/PHPStan/Rules/Exceptions/ThrowExprTypeRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/ThrowExprTypeRuleTest.php @@ -5,7 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -15,7 +15,7 @@ class ThrowExprTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ThrowExprTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false)); + return new ThrowExprTypeRule(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true)); } public function testRule(): void @@ -23,16 +23,16 @@ public function testRule(): void $this->analyse( [__DIR__ . '/data/throw-values.php'], [ - /*[ + [ 'Invalid type int to throw.', 29, ], [ - 'Invalid type ThrowValues\InvalidException to throw.', + 'Invalid type ThrowExprValues\InvalidException to throw.', 32, ], [ - 'Invalid type ThrowValues\InvalidInterfaceException to throw.', + 'Invalid type ThrowExprValues\InvalidInterfaceException to throw.', 35, ], [ @@ -40,10 +40,10 @@ public function testRule(): void 38, ], [ - 'Throwing object of an unknown class ThrowValues\NonexistentClass.', + '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, @@ -57,17 +57,14 @@ public function testClassExists(): void $this->analyse([__DIR__ . '/data/throw-class-exists.php'], []); } + #[RequiresPhp('>= 8.0')] public function testRuleWithNullsafeVariant(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/throw-values-nullsafe.php'], [ - /*[ + [ 'Invalid type Exception|null to throw.', 17, - ],*/ + ], ]); } diff --git a/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php index 1f32de8fe4..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 @@ -19,7 +20,7 @@ protected function getRule(): Rule return new ThrowExpressionRule($this->phpVersion); } - public function dataRule(): array + public static function dataRule(): array { return [ [ @@ -39,9 +40,9 @@ public function dataRule(): array } /** - * @dataProvider dataRule * @param list $expectedErrors */ + #[DataProvider('dataRule')] public function testRule(int $phpVersion, array $expectedErrors): void { $this->phpVersion = new PhpVersion($phpVersion); diff --git a/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php index ff6c4416a6..af4f7bc372 100644 --- a/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; use ThrowsVoidFunction\MyException; /** @@ -20,7 +21,7 @@ class ThrowsVoidFunctionWithExplicitThrowPointRuleTest extends RuleTestCase protected function getRule(): Rule { return new ThrowsVoidFunctionWithExplicitThrowPointRule(new DefaultExceptionTypeResolver( - $this->createReflectionProvider(), + self::createReflectionProvider(), [], [], [], @@ -28,7 +29,7 @@ protected function getRule(): Rule ), $this->missingCheckedExceptionInThrows); } - public function dataRule(): array + public static function dataRule(): array { return [ [ @@ -85,10 +86,10 @@ public function dataRule(): array } /** - * @dataProvider dataRule * @param string[] $checkedExceptionClasses * @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 5a2dcb0429..196d99a84c 100644 --- a/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php @@ -4,9 +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; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -22,7 +23,7 @@ class ThrowsVoidMethodWithExplicitThrowPointRuleTest extends RuleTestCase protected function getRule(): Rule { return new ThrowsVoidMethodWithExplicitThrowPointRule(new DefaultExceptionTypeResolver( - $this->createReflectionProvider(), + self::createReflectionProvider(), [], [], [], @@ -30,7 +31,7 @@ protected function getRule(): Rule ), $this->missingCheckedExceptionInThrows); } - public function dataRule(): array + public static function dataRule(): array { return [ [ @@ -87,10 +88,10 @@ public function dataRule(): array } /** - * @dataProvider dataRule * @param string[] $checkedExceptionClasses * @param list $errors */ + #[DataProvider('dataRule')] public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void { $this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows; @@ -98,11 +99,9 @@ public function testRule(bool $missingCheckedExceptionInThrows, array $checkedEx $this->analyse([__DIR__ . '/data/throws-void-method.php'], $errors); } + #[RequiresPhp('>= 8.0')] public function testBug6910(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->missingCheckedExceptionInThrows = false; $this->checkedExceptionClasses = [UnhandledMatchError::class]; $this->analyse([__DIR__ . '/data/bug-6910.php'], []); diff --git a/tests/PHPStan/Rules/Exceptions/ThrowsVoidPropertyHookWithExplicitThrowPointRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowsVoidPropertyHookWithExplicitThrowPointRuleTest.php new file mode 100644 index 0000000000..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..8de4ae7bed 100644 --- a/tests/PHPStan/Rules/Exceptions/TooWideFunctionThrowTypeRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/TooWideFunctionThrowTypeRuleTest.php @@ -11,14 +11,20 @@ class TooWideFunctionThrowTypeRuleTest extends RuleTestCase { + private bool $implicitThrows = true; + protected function getRule(): Rule { - return new TooWideFunctionThrowTypeRule(new TooWideThrowTypeCheck()); + return new TooWideFunctionThrowTypeRule(new TooWideThrowTypeCheck($this->implicitThrows)); } public function testRule(): void { $this->analyse([__DIR__ . '/data/too-wide-throws-function.php'], [ + [ + 'Function TooWideThrowsFunction\doFoo3() has InvalidArgumentException in PHPDoc @throws tag but it\'s not thrown.', + 20, + ], [ 'Function TooWideThrowsFunction\doFoo4() has DomainException in PHPDoc @throws tag but it\'s not thrown.', 26, diff --git a/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php b/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php index 98c3737887..85cff48d9b 100644 --- a/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php @@ -5,7 +5,8 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -13,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, @@ -55,10 +62,6 @@ public function testBug6233(): void public function testImmediatelyCalledArrowFunction(): void { - if (PHP_VERSION_ID < 70400) { - self::markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/immediately-called-arrow-function.php'], [ [ 'Method ImmediatelyCalledArrowFunction\ImmediatelyCalledCallback::doFoo2() has InvalidArgumentException in PHPDoc @throws tag but it\'s not thrown.', @@ -67,13 +70,37 @@ public function testImmediatelyCalledArrowFunction(): void ]); } + #[RequiresPhp('>= 8.1')] public function testFirstClassCallable(): void { - if (PHP_VERSION_ID < 80100) { - self::markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/immediately-called-fcc.php'], []); } + public static function dataRuleLookOnlyForExplicitThrowPoints(): iterable + { + yield [ + true, + [], + ]; + yield [ + false, + [ + [ + 'Method TooWideThrowsExplicit\Foo::doFoo() has Exception in PHPDoc @throws tag but it\'s not thrown.', + 11, + ], + ], + ]; + } + + /** + * @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..da40cc6d80 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/TooWidePropertyHookThrowTypeRuleTest.php @@ -0,0 +1,54 @@ + + */ +class TooWidePropertyHookThrowTypeRuleTest extends RuleTestCase +{ + + private bool $implicitThrows = true; + + protected function getRule(): Rule + { + return new TooWidePropertyHookThrowTypeRule(self::getContainer()->getByType(FileTypeMapper::class), new TooWideThrowTypeCheck($this->implicitThrows)); + } + + #[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/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 index 058f6c1a1f..dfc01e41c8 100644 --- a/tests/PHPStan/Rules/Exceptions/data/bug-4852.php +++ b/tests/PHPStan/Rules/Exceptions/data/bug-4852.php @@ -61,9 +61,17 @@ public function offsetUnset ($offset) {} try { $buz[] = 'value'; } catch (Exception $e) { - // not dead + // dead because $buz cannot be a subclass } +function (MaybeThrows2 $buz): void { + try { + $buz[] = 'value'; + } catch (Exception $e) { + // not dead + } +}; + $baz = new DefinitelyNoThrows(); try { $baz[] = 'value'; diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-5903.php b/tests/PHPStan/Rules/Exceptions/data/bug-5903.php index b4c12e3877..0300b6ecc8 100644 --- a/tests/PHPStan/Rules/Exceptions/data/bug-5903.php +++ b/tests/PHPStan/Rules/Exceptions/data/bug-5903.php @@ -2,7 +2,7 @@ namespace Bug5903; -class Test +final class Test { /** @var \Traversable */ protected $traversable; diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-6256.php b/tests/PHPStan/Rules/Exceptions/data/bug-6256.php index c7cb4766ad..ed14c55755 100644 --- a/tests/PHPStan/Rules/Exceptions/data/bug-6256.php +++ b/tests/PHPStan/Rules/Exceptions/data/bug-6256.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 += 7.4 + */ 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-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/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 a5c79a5ec8..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(); } 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 index 1c16fec53d..cad8c5348c 100644 --- a/tests/PHPStan/Rules/Exceptions/data/union-type-error.php +++ b/tests/PHPStan/Rules/Exceptions/data/union-type-error.php @@ -4,7 +4,7 @@ namespace UnionTypeError; -class Foo { +final class Foo { public string|int $stringOrInt; public string|array $stringOrArray; 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/disable-detect-multi-catch.neon b/tests/PHPStan/Rules/Exceptions/disable-detect-multi-catch.neon deleted file mode 100644 index e763557205..0000000000 --- a/tests/PHPStan/Rules/Exceptions/disable-detect-multi-catch.neon +++ /dev/null @@ -1,3 +0,0 @@ -parameters: - featureToggles: - detectDeadTypeInMultiCatch: false diff --git a/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php b/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php index 95ac9ed295..96be9603a8 100644 --- a/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php @@ -15,7 +15,11 @@ class ArrayFilterRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ArrayFilterRule($this->createReflectionProvider(), $this->treatPhpDocTypesAsCertain); + return new ArrayFilterRule( + self::createReflectionProvider(), + $this->treatPhpDocTypesAsCertain, + true, + ); } public function testFile(): void diff --git a/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php index c945b1efea..face9ea8f5 100644 --- a/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php @@ -16,7 +16,11 @@ class ArrayValuesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ArrayValuesRule($this->createReflectionProvider(), $this->treatPhpDocTypesAsCertain); + return new ArrayValuesRule( + self::createReflectionProvider(), + $this->treatPhpDocTypesAsCertain, + true, + ); } public function testFile(): void diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php index 7f3aab6ac2..64ce4730fe 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -23,25 +22,25 @@ 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, false, false, true, false), + 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(), ), true, ), diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php index bf46cd56a6..99d577361e 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,14 +17,14 @@ class ArrowFunctionReturnTypeRuleTest extends RuleTestCase protected function getRule(): Rule { return new ArrowFunctionReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper( - $this->createReflectionProvider(), + self::createReflectionProvider(), true, false, true, false, false, - true, false, + true, ))); } @@ -39,9 +39,17 @@ public function testRule(): void 'Anonymous function should return int but returns string.', 14, ], + + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testRuleNever(): void + { + $this->analyse([__DIR__ . '/data/arrow-function-never-return.php'], [ [ 'Anonymous function should never return but return statement found.', - 44, + 12, ], ]); } @@ -51,12 +59,9 @@ public function testBug3261(): void $this->analyse([__DIR__ . '/data/bug-3261.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug8179(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-8179.php'], []); } @@ -67,10 +72,6 @@ public function testBugSpaceship(): void public function testBugFunctionMethodConstants(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/bug-anonymous-function-method-constant.php'], []); } diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index bcd1c9ee87..837f9faf73 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -10,6 +9,8 @@ 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; /** @@ -22,19 +23,17 @@ class CallCallablesRuleTest extends RuleTestCase protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false, true, false); + $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, @@ -106,57 +105,62 @@ public function testRule(): void 106, ], [ - 'Trying to invoke CallCallables\Baz but it might not be a callable.', + 'Trying to invoke CallCallables\Baz but it\'s not a callable.', 113, ], + [ + 'Trying to invoke CallCallables\Baz but it might not be a callable.', + 122, + ], [ 'Trying to invoke array{object, \'bar\'} but it might not be a callable.', - 131, + 140, ], [ 'Closure invoked with 0 parameters, 3 required.', - 146, + 155, ], [ 'Closure invoked with 1 parameter, 3 required.', - 147, + 156, ], [ 'Closure invoked with 2 parameters, 3 required.', - 148, + 157, ], [ 'Trying to invoke array{object, \'yo\'} but it might not be a callable.', - 163, + 172, ], [ 'Trying to invoke array{object, \'yo\'} but it might not be a callable.', - 167, + 176, ], [ 'Trying to invoke array{\'CallCallables\\\\CallableInForeach\', \'bar\'|\'foo\'} but it might not be a callable.', - 179, + 188, ], [ 'Trying to invoke array{\'CallCallables\\\\ConstantArrayUnionCallables\'|\'DateTimeImmutable\', \'doFoo\'} but it might not be a callable.', - 205, + 214, ], [ 'Trying to invoke array{\'CallCallables\\\ConstantArrayUnionCallables\', \'doBaz\'|\'doFoo\'} but it might not be a callable.', - 212, + 221, ], ]; if (PHP_VERSION_ID >= 80000) { $errors[] = [ 'Trying to invoke array{\'CallCallables\\\ConstantArrayUnionCallables\'|\'CallCallables\\\ConstantArrayUnionCallablesTest\', \'doBar\'|\'doFoo\'} but it\'s not a callable.', - 220, + 229, ]; } $this->analyse([__DIR__ . '/data/callables.php'], $errors); } + #[RequiresPhp('>= 8.0')] public function testNamedArguments(): void { $this->analyse([__DIR__ . '/data/callables-named-arguments.php'], [ @@ -183,7 +187,7 @@ public function testNamedArguments(): void ]); } - public function dataBug3566(): array + public static function dataBug3566(): array { return [ [ @@ -203,21 +207,18 @@ public function dataBug3566(): array } /** - * @dataProvider dataBug3566 * @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 { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/callables-nullsafe.php'], [ [ diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 3ddad85c09..45e6e9fdfd 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -10,6 +9,8 @@ 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; @@ -21,12 +22,14 @@ class CallToFunctionParametersRuleTest extends RuleTestCase private bool $checkExplicitMixed = false; + 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, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, 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), ); } @@ -316,9 +319,6 @@ public function testImplodeOnPhp74(): void 8, ], ]; - if (PHP_VERSION_ID < 70400) { - $errors = []; - } if (PHP_VERSION_ID >= 80000) { $errors = [ [ @@ -333,7 +333,6 @@ public function testImplodeOnPhp74(): void public function testImplodeOnLessThanPhp74(): void { - $errors = []; if (PHP_VERSION_ID >= 80000) { $errors = [ [ @@ -341,7 +340,7 @@ public function testImplodeOnLessThanPhp74(): void 8, ], ]; - } elseif (PHP_VERSION_ID >= 70400) { + } else { $errors = [ [ 'Parameter #1 $glue of function implode expects string, array given.', @@ -424,12 +423,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 ]); @@ -477,6 +473,7 @@ public function testGenericFunction(): void ]); } + #[RequiresPhp('>= 8.0')] public function testNamedArguments(): void { $errors = [ @@ -493,17 +490,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'], []); @@ -536,17 +538,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'], []); } @@ -636,15 +640,15 @@ public function testArrayUdiffCallback(): void 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&non-falsy-string&numeric-string) given.', + 'Parameter #3 $data_comp_func of function array_udiff expects callable(1|2|3|4|5|6, 1|2|3|4|5|6): int, Closure(int, int): (literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string) given.', 14, ], [ - 'Parameter #1 $arr1 of function array_udiff expects array, null given.', + '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, null given.', + 'Parameter #2 $arr2 of function array_udiff expects array<(int&TK)|(string&TK), string>, null given.', 21, ], [ @@ -658,19 +662,19 @@ public function testPregReplaceCallback(): void { $this->analyse([__DIR__ . '/data/preg_replace_callback.php'], [ [ - 'Parameter #2 $callback of function preg_replace_callback expects callable(array): string, Closure(string): string given.', + 'Parameter #2 $callback of function preg_replace_callback expects callable(array): string, Closure(string): string given.', 6, ], [ - 'Parameter #2 $callback of function preg_replace_callback expects callable(array): string, Closure(string): string given.', + '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.', + '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.', + 'Parameter #2 $callback of function preg_replace_callback expects callable(array): string, Closure(): void given.', 25, ], ]); @@ -791,12 +795,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.', @@ -815,10 +816,6 @@ 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 list|string, array{something: 'bogus', in: 'here'} given.", @@ -834,7 +831,7 @@ public function testBug5609(): void $this->analyse([__DIR__ . '/data/bug-5609.php'], []); } - public function dataArrayMapMultiple(): array + public static function dataArrayMapMultiple(): array { return [ [true], @@ -842,9 +839,7 @@ public function dataArrayMapMultiple(): array ]; } - /** - * @dataProvider dataArrayMapMultiple - */ + #[DataProvider('dataArrayMapMultiple')] public function testArrayMapMultiple(bool $checkExplicitMixed): void { $this->checkExplicitMixed = $checkExplicitMixed; @@ -856,7 +851,7 @@ public function testArrayMapMultiple(bool $checkExplicitMixed): void ]); } - public function dataArrayFilterCallback(): array + public static function dataArrayFilterCallback(): array { return [ [true], @@ -864,9 +859,7 @@ public function dataArrayFilterCallback(): array ]; } - /** - * @dataProvider dataArrayFilterCallback - */ + #[DataProvider('dataArrayFilterCallback')] public function testArrayFilterCallback(bool $checkExplicitMixed): void { $this->checkExplicitMixed = $checkExplicitMixed; @@ -886,6 +879,112 @@ public function testArrayFilterCallback(bool $checkExplicitMixed): void $this->analyse([__DIR__ . '/data/array_filter_callback.php'], $errors); } + #[RequiresPhp('>= 8.4')] + public function testArrayAllCallback(): void + { + $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'], [ @@ -1114,12 +1213,9 @@ public function testDiscussion7450WithCheckExplicitMixed(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug7211(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1'); - } - $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/bug-7211.php'], []); } @@ -1150,11 +1246,9 @@ public function testBug7676(): void $this->analyse([__DIR__ . '/data/bug-7676.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug7138(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1'); - } $this->analyse([__DIR__ . '/data/bug-7138.php'], []); } @@ -1211,32 +1305,52 @@ public function testCurlSetOpt(): void 17, ], [ - 'Parameter #3 $value of function curl_setopt expects bool, int given.', + '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 bool, string given.', + 'Parameter #3 $value of function curl_setopt expects string, int given.', 20, ], [ - 'Parameter #3 $value of function curl_setopt expects int, string given.', + '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.', - 24, + 27, ], [ 'Parameter #3 $value of function curl_setopt expects resource, string given.', - 26, + 29, ], [ 'Parameter #3 $value of function curl_setopt expects array|string, int given.', - 28, + 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.', - 67, + 77, ], ]); } @@ -1337,12 +1451,9 @@ public function testFilterInputType(): void $this->analyse([__DIR__ . '/data/filter-input-type.php'], $errors); } + #[RequiresPhp('>= 8.0')] public function testBug9283(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->analyse([__DIR__ . '/data/bug-9283.php'], []); } @@ -1387,12 +1498,9 @@ public function testFlockParams(): void ]); } + #[RequiresPhp('>= 8.3')] public function testJsonValidate(): void { - if (PHP_VERSION_ID < 80300) { - $this->markTestSkipped('Test requires PHP 8.3'); - } - $this->analyse([__DIR__ . '/data/json_validate.php'], [ [ 'Parameter #2 $depth of function json_validate expects int<1, max>, 0 given.', @@ -1415,17 +1523,15 @@ 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 { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1'); - } - $this->analyse([__DIR__ . '/data/bug-9699.php'], [ [ 'Parameter #1 $f of function Bug9699\int_int_int_string expects Closure(int, int, int, string): int, Closure(int, int, int ...): int given.', @@ -1449,12 +1555,9 @@ public function testBug9803(): void $this->analyse([__DIR__ . '/data/bug-9803.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug9018(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->analyse([__DIR__ . '/data/bug-9018.php'], [ [ 'Unknown parameter $str1 in call to function levenshtein.', @@ -1475,39 +1578,33 @@ public function testBug9018(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug9399(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->analyse([__DIR__ . '/data/bug-9399.php'], []); } - public function testBug9923(): void + #[RequiresPhp('>= 8.0')] + public function testBug9559(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } + $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 { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->analyse([__DIR__ . '/data/bug-9823.php'], []); } + #[RequiresPhp('>= 8.0')] public function testNamedParametersForMultiVariantFunctions(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->analyse([__DIR__ . '/data/call-to-function-named-params-multivariant.php'], []); } @@ -1548,21 +1645,15 @@ public function testBug9793(): void $this->analyse([__DIR__ . '/data/bug-9793.php'], $errors); } + #[RequiresPhp('>= 8.0')] public function testCallToArrayFilterWithNullCallback(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->analyse([__DIR__ . '/data/array_filter_null_callback.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug10171(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->analyse([__DIR__ . '/data/bug-10171.php'], [ [ 'Unknown parameter $samesite in call to function setcookie.', @@ -1598,6 +1689,7 @@ 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'], []); @@ -1624,10 +1716,6 @@ public function testDiscussion10454(): void public function testBug10527(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4'); - } - $this->analyse([__DIR__ . '/data/bug-10527.php'], []); } @@ -1650,12 +1738,13 @@ public function testArgon2PasswordHash(): void $this->analyse([__DIR__ . '/data/argon2id-password-hash.php'], []); } - public function testParamClosureThis(): void + public function testBug4960(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } + $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.', @@ -1668,17 +1757,15 @@ public function testParamClosureThis(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug10297(): void { $this->analyse([__DIR__ . '/data/bug-10297.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug10974(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/bug-10974.php'], []); } @@ -1687,22 +1774,22 @@ public function testCountArrayShift(): void if (PHP_VERSION_ID < 80000) { $errors = [ [ - 'Parameter #1 $var of function count expects array|Countable, array|false given.', + '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.', + '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.', + '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.', + 'Parameter #1 $value of function count expects array|Countable, array|false given.', 16, ], ]; @@ -1711,9 +1798,422 @@ public function testCountArrayShift(): void $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 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'], []); + } + + #[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, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php index 85c1f400ee..28257e5a80 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php @@ -14,7 +14,7 @@ class CallToFunctionStatementWithoutSideEffectsRuleTest extends RuleTestCase protected function getRule(): Rule { - return new CallToFunctionStatementWithoutSideEffectsRule($this->createReflectionProvider()); + return new CallToFunctionStatementWithoutSideEffectsRule(self::createReflectionProvider()); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php index 15734fdf18..00932fa18f 100644 --- a/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -14,7 +14,12 @@ class CallToNonExistentFunctionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new CallToNonExistentFunctionRule($this->createReflectionProvider(), true); + return new CallToNonExistentFunctionRule(self::createReflectionProvider(), true, true); + } + + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; } public function testEmptyFile(): void @@ -95,12 +100,9 @@ public function testMatchExprAnalysis(): 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.', @@ -150,12 +152,9 @@ public function testCallToRemovedFunctionsOnPhp8(): void ]); } + #[RequiresPhp('>= 8.0')] public function testCreateFunctionPhp8(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/create_function.php'], [ [ 'Function create_function not found.', @@ -165,12 +164,9 @@ 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'], []); } @@ -215,21 +211,15 @@ public function testBug7952(): void $this->analyse([__DIR__ . '/data/bug-7952.php'], []); } + #[RequiresPhp('>= 8.2')] public function testBug8058(): void { - if (PHP_VERSION_ID < 80200) { - $this->markTestSkipped('Test requires PHP 8.2'); - } - $this->analyse([__DIR__ . '/../Methods/data/bug-8058.php'], []); } + #[RequiresPhp('< 8.2')] public function testBug8058b(): void { - if (PHP_VERSION_ID >= 80200) { - $this->markTestSkipped('Test requires PHP before 8.2'); - } - $this->analyse([__DIR__ . '/../Methods/data/bug-8058.php'], [ [ 'Function mysqli_execute_query not found.', @@ -258,4 +248,15 @@ public function testBug10003(): void ]); } + 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 index 9eed739399..ba44a5bc0f 100644 --- a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -10,7 +9,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -20,16 +19,13 @@ class CallUserFuncRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); - return new CallUserFuncRule($reflectionProvider, new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, true, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true)); + $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 { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/call-user-func.php'], [ [ 'Callable passed to call_user_func() invoked with 0 parameters, 1 required.', @@ -83,4 +79,27 @@ 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 f4a3d5a1bb..659659449e 100644 --- a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -23,25 +22,25 @@ 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, false, false, true, false), + 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(), ), true, ), diff --git a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php index 020000b38a..38d4ec65d8 100644 --- a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php @@ -6,7 +6,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -16,7 +15,7 @@ class ClosureReturnTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ClosureReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false))); + return new ClosureReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true))); } public function testClosureReturnTypeRule(): void @@ -131,10 +130,6 @@ public function testBug7220(): void public function testBugFunctionMethodConstants(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/bug-anonymous-function-method-constant.php'], []); } diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php index 2b2cec639b..b7d14e5ee6 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php @@ -10,6 +10,8 @@ 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; /** @@ -22,13 +24,15 @@ class ExistingClassesInArrowFunctionTypehintsRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $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), @@ -53,7 +57,7 @@ public function testRule(): void ]); } - public function dataNativeUnionTypes(): array + public static function dataNativeUnionTypes(): array { return [ [ @@ -77,16 +81,16 @@ public function dataNativeUnionTypes(): array } /** - * @dataProvider dataNativeUnionTypes * @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 [ [ @@ -237,16 +241,17 @@ public function dataRequiredParameterAfterOptional(): array } /** - * @dataProvider dataRequiredParameterAfterOptional * @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-arrow.php'], $errors); } - public function dataIntersectionTypes(): array + public static function dataIntersectionTypes(): array { return [ [80000, []], @@ -275,9 +280,9 @@ public function dataIntersectionTypes(): array } /** - * @dataProvider dataIntersectionTypes * @param list $errors */ + #[DataProvider('dataIntersectionTypes')] public function testIntersectionTypes(int $phpVersion, array $errors): void { $this->phpVersionId = $phpVersion; @@ -288,7 +293,14 @@ public function testIntersectionTypes(int $phpVersion, array $errors): void public function testNever(): void { $errors = []; - if (PHP_VERSION_ID < 80200) { + 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.', diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php index 439f5bdd71..b01e0f5230 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php @@ -10,6 +10,8 @@ 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; /** @@ -22,13 +24,15 @@ class ExistingClassesInClosureTypehintsRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $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), @@ -97,7 +101,7 @@ public function testVoidParameterTypehint(): void ]); } - public function dataNativeUnionTypes(): array + public static function dataNativeUnionTypes(): array { return [ [ @@ -121,16 +125,16 @@ public function dataNativeUnionTypes(): array } /** - * @dataProvider dataNativeUnionTypes * @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 [ [ @@ -281,16 +285,17 @@ public function dataRequiredParameterAfterOptional(): array } /** - * @dataProvider dataRequiredParameterAfterOptional * @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 function dataIntersectionTypes(): array + public static function dataIntersectionTypes(): array { return [ [80000, []], @@ -319,9 +324,9 @@ public function dataIntersectionTypes(): array } /** - * @dataProvider dataIntersectionTypes * @param list $errors */ + #[DataProvider('dataIntersectionTypes')] public function testIntersectionTypes(int $phpVersion, array $errors): void { $this->phpVersionId = $phpVersion; @@ -329,4 +334,23 @@ public function testIntersectionTypes(int $phpVersion, array $errors): void $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, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php index 227044f2d7..e9ec0e10b9 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php @@ -10,6 +10,8 @@ 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; /** @@ -22,13 +24,15 @@ class ExistingClassesInTypehintsRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $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), @@ -178,7 +182,7 @@ public function testVoidParameterTypehint(): void ]); } - public function dataNativeUnionTypes(): array + public static function dataNativeUnionTypes(): array { return [ [ @@ -202,16 +206,16 @@ public function dataNativeUnionTypes(): array } /** - * @dataProvider dataNativeUnionTypes * @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 [ [ @@ -362,16 +366,17 @@ public function dataRequiredParameterAfterOptional(): array } /** - * @dataProvider dataRequiredParameterAfterOptional * @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 function dataIntersectionTypes(): array + public static function dataIntersectionTypes(): array { return [ [80000, []], @@ -400,9 +405,9 @@ public function dataIntersectionTypes(): array } /** - * @dataProvider dataIntersectionTypes * @param list $errors */ + #[DataProvider('dataIntersectionTypes')] public function testIntersectionTypes(int $phpVersion, array $errors): void { $this->phpVersionId = $phpVersion; @@ -410,27 +415,23 @@ public function testIntersectionTypes(int $phpVersion, array $errors): void $this->analyse([__DIR__ . '/data/intersection-types.php'], $errors); } - public function dataTrueTypes(): array + public function testTrueTypehint(): void { - return [ - [ - 80200, - [], - ], - ]; - } - - /** - * @dataProvider dataTrueTypes - * @param list $errors - */ - public function testTrueTypehint(int $phpVersion, array $errors): void - { - $this->phpVersionId = $phpVersion; + 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'], [ @@ -441,6 +442,7 @@ public function testConditionalReturnType(): void ]); } + #[RequiresPhp('>= 8.0')] public function testTemplateInParamOut(): void { $this->analyse([__DIR__ . '/data/param-out.php'], [ @@ -451,4 +453,40 @@ public function testTemplateInParamOut(): void ]); } + public function testParamOutClasses(): void + { + $this->analyse([__DIR__ . '/data/param-out-classes.php'], [ + [ + 'Parameter $p of function ParamOutClasses\doFoo() has invalid type ParamOutClasses\Nonexistent.', + 20, + ], + [ + 'Parameter $q of function ParamOutClasses\doFoo() has invalid type ParamOutClasses\FooTrait.', + 20, + ], + [ + 'Class ParamOutClasses\Foo referenced with incorrect case: ParamOutClasses\fOO.', + 20, + ], + ]); + } + + public function testParamClosureThisClasses(): void + { + $this->analyse([__DIR__ . '/data/param-closure-this-classes.php'], [ + [ + 'Parameter $a of function ParamClosureThisClassesFunctions\doFoo() has invalid type ParamClosureThisClassesFunctions\Nonexistent.', + 21, + ], + [ + 'Parameter $b of function ParamClosureThisClassesFunctions\doFoo() has invalid type ParamClosureThisClassesFunctions\FooTrait.', + 22, + ], + [ + 'Class ParamClosureThisClassesFunctions\Foo referenced with incorrect case: ParamClosureThisClassesFunctions\fOO.', + 23, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php index 553ab6c462..0ad48b3e98 100644 --- a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -23,25 +22,25 @@ 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, false, false, true, false), + 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(), ), true, ), diff --git a/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php index 2a92e5f5a2..9db721eaee 100644 --- a/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use const PHP_VERSION_ID; /** @@ -16,22 +17,20 @@ class FunctionCallableRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new FunctionCallableRule( $reflectionProvider, - new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new PhpVersion(PHP_VERSION_ID), true, true, ); } + #[RequiresPhp('< 8.1')] public function testNotSupportedOnOlderVersions(): void { - if (PHP_VERSION_ID >= 80100) { - self::markTestSkipped('Test runs on PHP < 8.1.'); - } $this->analyse([__DIR__ . '/data/function-callable-not-supported.php'], [ [ 'First-class callables are supported only on PHP 8.1 and later.', @@ -40,12 +39,9 @@ public function testNotSupportedOnOlderVersions(): void ]); } + #[RequiresPhp('>= 8.1')] public function testRule(): void { - if (PHP_VERSION_ID < 80100) { - self::markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/function-callable.php'], [ [ 'Function nonexistent not found.', diff --git a/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php deleted file mode 100644 index 44755df63d..0000000000 --- a/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php +++ /dev/null @@ -1,61 +0,0 @@ - - */ -class ImplodeFunctionRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - $broker = $this->createReflectionProvider(); - return new ImplodeFunctionRule($broker, new RuleLevelHelper($broker, true, false, true, false, false, true, false), 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, - ], - ]); - } - - public function testBug6000(): void - { - $this->analyse([__DIR__ . '/../Arrays/data/bug-6000.php'], []); - } - - public function testBug8467a(): void - { - $this->analyse([__DIR__ . '/../Arrays/data/bug-8467a.php'], []); - } - -} diff --git a/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php index 25f0facf27..d0f1b9a809 100644 --- a/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -16,16 +16,13 @@ class ImplodeParameterCastableToStringRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ImplodeParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, true, false))); + $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 { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/implode-param-castable-to-string-functions-named-args.php'], [ [ 'Parameter $array of function implode expects array, array> given.', @@ -46,12 +43,9 @@ public function testNamedArguments(): void ]); } + #[RequiresPhp('>= 8.1')] public function testEnum(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/implode-param-castable-to-string-functions-enum.php'], [ [ 'Parameter #2 $array of function implode expects array, array given.', @@ -64,7 +58,7 @@ public function testImplode(): void { $this->analyse([__DIR__ . '/data/implode.php'], [ [ - 'Parameter #2 $array of function implode expects array, array|string> given.', + 'Parameter #2 $array of function implode expects array, array|string> given.', 9, ], [ @@ -100,4 +94,14 @@ 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 index 9ec40a74a9..b6a79b6817 100644 --- a/tests/PHPStan/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRuleTest.php @@ -4,7 +4,6 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -19,9 +18,6 @@ protected function getRule(): Rule public function testRule(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $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.', diff --git a/tests/PHPStan/Rules/Functions/IncompatibleClosureFunctionDefaultParameterTypeRuleTest.php b/tests/PHPStan/Rules/Functions/IncompatibleClosureFunctionDefaultParameterTypeRuleTest.php index 8d0506f9fc..88ce97861e 100644 --- a/tests/PHPStan/Rules/Functions/IncompatibleClosureFunctionDefaultParameterTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/IncompatibleClosureFunctionDefaultParameterTypeRuleTest.php @@ -4,7 +4,6 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -19,9 +18,6 @@ protected function getRule(): Rule public function testRule(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $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.', diff --git a/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php index a12ee381e9..c88fb550da 100644 --- a/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php @@ -14,7 +14,7 @@ class MissingFunctionParameterTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - return new MissingFunctionParameterTypehintRule(new MissingTypehintCheck(true, true, true, true, []), true); + return new MissingFunctionParameterTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php index 37cd3ffb81..2b64aba5ba 100644 --- a/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php @@ -14,7 +14,7 @@ class MissingFunctionReturnTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - return new MissingFunctionReturnTypehintRule(new MissingTypehintCheck(true, true, true, true, [])); + return new MissingFunctionReturnTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php index 60d620474c..0b0296bc6a 100644 --- a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -23,25 +22,25 @@ 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, false, false, true, false), + 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(), ), true, ), 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 index 8619497c4f..bfa7339c18 100644 --- a/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use function array_map; use function str_replace; use const PHP_VERSION_ID; @@ -18,8 +19,8 @@ class ParameterCastableToStringRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, true, false))); + $broker = self::createReflectionProvider(); + return new ParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, true, true, false, true))); } public function testRule(): void @@ -68,12 +69,9 @@ public function testRule(): void ])); } + #[RequiresPhp('>= 8.0')] public function testNamedArguments(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/param-castable-to-string-functions-named-args.php'], [ [ 'Parameter $keys of function array_combine expects an array of values castable to string, array> given.', @@ -86,12 +84,9 @@ public function testNamedArguments(): void ]); } + #[RequiresPhp('>= 8.1')] public function testEnum(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/param-castable-to-string-functions-enum.php'], [ [ 'Parameter #1 $array of function array_intersect expects an array of values castable to string, array given.', @@ -154,18 +149,15 @@ 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|Bug3946\stdClass|float|int|string> given.', + '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 { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-11111.php'], [ [ 'Parameter #1 $keys of function array_fill_keys expects an array of values castable to string, array given.', @@ -178,12 +170,9 @@ public function testBug11111(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug11141(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-11141.php'], [ [ 'Parameter #1 $array of function array_diff expects an array of values castable to string, array given.', @@ -196,6 +185,20 @@ public function testBug11141(): void ]); } + 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 diff --git a/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php index d9f2375bfc..d799e58579 100644 --- a/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php @@ -17,7 +17,7 @@ protected function getRule(): Rule { return new PrintfArrayParametersRule( new PrintfHelper(new PhpVersion(PHP_VERSION_ID)), - $this->createReflectionProvider(), + self::createReflectionProvider(), ); } diff --git a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php index 252f2919ec..d60b1e0be6 100644 --- a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php @@ -17,7 +17,7 @@ protected function getRule(): Rule { return new PrintfParametersRule( new PrintfHelper(new PhpVersion(PHP_VERSION_ID)), - $this->createReflectionProvider(), + self::createReflectionProvider(), ); } diff --git a/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php b/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php index 4b9ab4e23c..39160f054f 100644 --- a/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Functions; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_INT_SIZE; @@ -14,7 +15,7 @@ class RandomIntParametersRuleTest extends RuleTestCase protected function getRule(): Rule { - return new RandomIntParametersRule($this->createReflectionProvider(), true); + return new RandomIntParametersRule(self::createReflectionProvider(), new PhpVersion(80000), true); } public function testFile(): void diff --git a/tests/PHPStan/Rules/Functions/ReturnNullsafeByRefRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnNullsafeByRefRuleTest.php index 92b6429eb0..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 @@ -35,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 45ddca1767..663f2ae211 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -20,7 +20,7 @@ class ReturnTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), $this->checkNullables, false, true, $this->checkExplicitMixed, false, true, false))); + return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper(self::createReflectionProvider(), $this->checkNullables, false, true, $this->checkExplicitMixed, false, false, true))); } public function testReturnTypeRule(): void @@ -173,7 +173,7 @@ public function testListWithNullablesChecked(): void $this->checkNullables = true; $this->analyse([__DIR__ . '/data/return-list-nullables.php'], [ [ - 'Function ReturnListNullables\doFoo() should return array|null but returns array.', + 'Function ReturnListNullables\doFoo() should return array|null but returns list.', 16, ], ]); @@ -225,6 +225,7 @@ public function testBug7766(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug8846(): void { $this->checkExplicitMixed = true; @@ -232,12 +233,9 @@ public function testBug8846(): void $this->analyse([__DIR__ . '/data/bug-8846.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug10077(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->checkExplicitMixed = true; $this->checkNullables = true; $this->analyse([__DIR__ . '/data/bug-10077.php'], [ @@ -255,6 +253,7 @@ public function testBug8683(): void $this->analyse([__DIR__ . '/data/bug-8683.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug7984(): void { $this->checkExplicitMixed = true; @@ -283,6 +282,13 @@ public function testBug10732(): void $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; @@ -290,4 +296,78 @@ public function testBug11518(): void $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 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, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php index 98076839d1..8193d5b6ef 100644 --- a/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php +++ b/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use function array_map; use function str_replace; use const PHP_VERSION_ID; @@ -18,15 +19,15 @@ class SortParameterCastableToStringRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new SortParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, true, false))); + $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.', + 'Parameter #1 $array of function array_unique expects an array of values castable to string, array> given.', 16, ], [ @@ -38,27 +39,27 @@ public function testRule(): void 20, ], [ - 'Parameter #1 $array of function rsort expects an array of values castable to string, array> given.', + '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, array> given.', + '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.', + '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.', + '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, array> given.', + '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, array> given.', + 'Parameter #1 $array of function asort expects an array of values castable to string, list> given.', 27, ], [ @@ -70,25 +71,22 @@ public function testRule(): void 32, ], [ - 'Parameter #1 $array of function sort expects an array of values castable to string, array> given.', + '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, array> given.', + '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 { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/sort-param-castable-to-string-functions-named-args.php'], [ [ - 'Parameter $array of function array_unique expects an array of values castable to string, array> given.', + 'Parameter $array of function array_unique expects an array of values castable to string, array> given.', 7, ], [ @@ -96,26 +94,23 @@ public function testNamedArguments(): void 9, ], [ - 'Parameter $array of function rsort expects an array of values castable to string, array> given.', + '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, array> given.', + '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.', + 'Parameter $array of function arsort expects an array of values castable to string, array> given.', 12, ], ]); } + #[RequiresPhp('>= 8.1')] public function testEnum(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/sort-param-castable-to-string-functions-enum.php'], [ [ 'Parameter #1 $array of function array_unique expects an array of values castable to string, array given.', @@ -126,11 +121,11 @@ public function testEnum(): void 14, ], [ - 'Parameter #1 $array of function rsort expects an array of values castable to string, array given.', + '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, array given.', + 'Parameter #1 $array of function asort expects an array of values castable to string, list given.', 16, ], [ @@ -145,6 +140,16 @@ 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 diff --git a/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php b/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php index 0d033268d8..137257162c 100644 --- a/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php @@ -14,7 +14,7 @@ class UnusedClosureUsesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UnusedClosureUsesRule(new UnusedFunctionParametersCheck($this->createReflectionProvider())); + return new UnusedClosureUsesRule(new UnusedFunctionParametersCheck(self::createReflectionProvider(), true)); } public function testUnusedClosureUses(): void @@ -22,11 +22,11 @@ public function testUnusedClosureUses(): void $this->analyse([__DIR__ . '/data/unused-closure-uses.php'], [ [ 'Anonymous function has an unused use $unused.', - 3, + 6, ], [ 'Anonymous function has an unused use $anotherUnused.', - 3, + 7, ], [ 'Anonymous function has an unused use $usedInClosureUse.', diff --git a/tests/PHPStan/Rules/Functions/UselessFunctionReturnValueRuleTest.php b/tests/PHPStan/Rules/Functions/UselessFunctionReturnValueRuleTest.php index 422103bd37..c71f59ec2e 100644 --- a/tests/PHPStan/Rules/Functions/UselessFunctionReturnValueRuleTest.php +++ b/tests/PHPStan/Rules/Functions/UselessFunctionReturnValueRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -15,7 +15,7 @@ class UselessFunctionReturnValueRuleTest extends RuleTestCase protected function getRule(): Rule { return new UselessFunctionReturnValueRule( - $this->createReflectionProvider(), + self::createReflectionProvider(), ); } @@ -37,12 +37,9 @@ public function testUselessReturnValue(): void ]); } + #[RequiresPhp('>= 8.0')] public function testUselessReturnValuePhp8(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/useless-fn-return-php8.php'], [ [ 'Return value of function print_r() is always true and the result is printed instead of being returned. Pass in true as parameter #2 $return to return the output instead.', diff --git a/tests/PHPStan/Rules/Functions/data/array_all.php b/tests/PHPStan/Rules/Functions/data/array_all.php new file mode 100644 index 0000000000..e4a3348eb3 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_all.php @@ -0,0 +1,56 @@ += 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_find.php b/tests/PHPStan/Rules/Functions/data/array_find.php new file mode 100644 index 0000000000..3b7d7f9e13 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_find.php @@ -0,0 +1,56 @@ += 8.4 + +// ok +array_find( + ['foo' => 1, 'bar' => 2], + function($value, $key) { + return $key === 0; + } +); + +// ok +array_find( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key === 0; + } +); + +// bad parameters +array_find( + ['foo' => 1, 'bar' => 2], + function(string $value, int $key): bool { + return $key === 0; + } +); + +// bad parameters +array_find( + ['foo' => 1, 'bar' => 2], + fn (string $item, int $key) => $key === 0, +); + +// bad return type +array_find( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key; + }, +); + +if (is_array($array)) { + // ok + array_find($array, fn ($value, $key) => $key === 0); + + // ok + array_find($array, fn (string $value, int $key) => $key === 0); + + // ok + array_find($array, fn (string $value) => $key === 0); + + // bad parameters + array_find($array, fn (string $item, array $key) => $key === 0); + + // bad return type + array_find($array, fn (string $value, int $key): array => []); +} diff --git a/tests/PHPStan/Rules/Functions/data/array_find_key.php b/tests/PHPStan/Rules/Functions/data/array_find_key.php new file mode 100644 index 0000000000..ab2b7df3fb --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_find_key.php @@ -0,0 +1,56 @@ += 8.4 + +// ok +array_find_key( + ['foo' => 1, 'bar' => 2], + function($value, $key) { + return $key === 0; + } +); + +// ok +array_find_key( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key === 0; + } +); + +// bad parameters +array_find_key( + ['foo' => 1, 'bar' => 2], + function(string $value, int $key): bool { + return $key === 0; + } +); + +// bad parameters +array_find_key( + ['foo' => 1, 'bar' => 2], + fn (string $item, int $key) => $key === 0, +); + +// bad return type +array_find_key( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key; + }, +); + +if (is_array($array)) { + // ok + array_find_key($array, fn ($value, $key) => $key === 0); + + // ok + array_find_key($array, fn (string $value, int $key) => $key === 0); + + // ok + array_find_key($array, fn (string $value) => $value === 'foo'); + + // bad parameters + array_find_key($array, fn (string $item, array $key) => $key === 0); + + // bad return type + array_find_key($array, fn (string $value, int $key): array => []); +} diff --git a/tests/PHPStan/Rules/Functions/data/array_intersect_uassoc.php b/tests/PHPStan/Rules/Functions/data/array_intersect_uassoc.php new file mode 100644 index 0000000000..9f8ba1bfa3 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_intersect_uassoc.php @@ -0,0 +1,33 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_intersect_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_intersect_uassoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_intersect_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_intersect_ukey.php b/tests/PHPStan/Rules/Functions/data/array_intersect_ukey.php new file mode 100644 index 0000000000..4d81086d24 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_intersect_ukey.php @@ -0,0 +1,33 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_intersect_ukey( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_intersect_ukey( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_intersect_ukey( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_reduce_arrow.php b/tests/PHPStan/Rules/Functions/data/array_reduce_arrow.php index 0606d7a275..d93d7b6ffc 100644 --- a/tests/PHPStan/Rules/Functions/data/array_reduce_arrow.php +++ b/tests/PHPStan/Rules/Functions/data/array_reduce_arrow.php @@ -1,4 +1,4 @@ -= 7.4 + 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_walk_arrow.php b/tests/PHPStan/Rules/Functions/data/array_walk_arrow.php index ca75b4998b..22a69296f1 100644 --- a/tests/PHPStan/Rules/Functions/data/array_walk_arrow.php +++ b/tests/PHPStan/Rules/Functions/data/array_walk_arrow.php @@ -1,4 +1,4 @@ -= 7.4 + 1, 'bar' => 2]; array_walk( diff --git a/tests/PHPStan/Rules/Functions/data/arrow-function-attributes.php b/tests/PHPStan/Rules/Functions/data/arrow-function-attributes.php index fa1ec0f17a..30f1a93884 100644 --- a/tests/PHPStan/Rules/Functions/data/arrow-function-attributes.php +++ b/tests/PHPStan/Rules/Functions/data/arrow-function-attributes.php @@ -1,4 +1,4 @@ -= 7.4 += 8.1 + +namespace ArrowFunctionNeverReturn; + +class Baz +{ + + public function doFoo(): void + { + $f = fn () => throw new \Exception(); + $g = fn (): never => throw new \Exception(); + $g = fn (): never => 1; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/arrow-function-never.php b/tests/PHPStan/Rules/Functions/data/arrow-function-never.php index 227ad163b2..da2cc65566 100644 --- a/tests/PHPStan/Rules/Functions/data/arrow-function-never.php +++ b/tests/PHPStan/Rules/Functions/data/arrow-function-never.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 + yield $value; - -class Baz -{ - - public function doFoo(): void - { - $f = fn () => throw new \Exception(); - $g = fn (): never => throw new \Exception(); - $g = fn (): never => 1; - } - -} diff --git a/tests/PHPStan/Rules/Functions/data/bug-10020.php b/tests/PHPStan/Rules/Functions/data/bug-10020.php new file mode 100644 index 0000000000..d59791a868 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10020.php @@ -0,0 +1,22 @@ +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-10499.php b/tests/PHPStan/Rules/Functions/data/bug-10499.php new file mode 100644 index 0000000000..d7cdb6c163 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10499.php @@ -0,0 +1,11 @@ + 'bar']); +lowerCaseKey(['FOO' => 'bar']); 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-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-11301.php b/tests/PHPStan/Rules/Functions/data/bug-11301.php new file mode 100644 index 0000000000..5c9f80cc08 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11301.php @@ -0,0 +1,48 @@ + + */ +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-12317.php b/tests/PHPStan/Rules/Functions/data/bug-12317.php new file mode 100644 index 0000000000..2443fefc09 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-12317.php @@ -0,0 +1,31 @@ +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 @@ +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-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 + 11); + + password_hash($password, PASSWORD_DEFAULT, $options); + } +} 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 + 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-8046.php b/tests/PHPStan/Rules/Functions/data/bug-8046.php new file mode 100644 index 0000000000..368f656341 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-8046.php @@ -0,0 +1,11 @@ + 7]; + +var_dump(add(...$args, b: 8)); 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 @@ += 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-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-9697.php b/tests/PHPStan/Rules/Functions/data/bug-9697.php index 742a5937cf..9f1c85e0e1 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-9697.php +++ b/tests/PHPStan/Rules/Functions/data/bug-9697.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 += 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/curl_setopt.php b/tests/PHPStan/Rules/Functions/data/curl_setopt.php index 76d3136a2d..bc5b8f6cea 100644 --- a/tests/PHPStan/Rules/Functions/data/curl_setopt.php +++ b/tests/PHPStan/Rules/Functions/data/curl_setopt.php @@ -15,6 +15,9 @@ public function errors(int $i, string $s) { // expecting string curl_setopt($curl, CURLOPT_URL, $i); curl_setopt($curl, CURLOPT_HTTPHEADER, $i); + curl_setopt($curl, CURLOPT_ABSTRACT_UNIX_SOCKET, null); + curl_setopt($curl, CURLOPT_ENCODING, $i); + curl_setopt($curl, CURLOPT_ACCEPT_ENCODING, $i); // expecting bool curl_setopt($curl, CURLOPT_AUTOREFERER, $i); curl_setopt($curl, CURLOPT_RETURNTRANSFER, $s); @@ -26,6 +29,9 @@ public function errors(int $i, string $s) { curl_setopt($curl, CURLOPT_FILE, $s); // expecting string or array curl_setopt($curl, CURLOPT_POSTFIELDS, $i); + // expecting non empty string + curl_setopt($curl, CURLOPT_URL, ''); + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, ''); } /** @@ -41,6 +47,8 @@ public function allGood(string $url, array $header) { curl_setopt($curl, CURLOPT_AUTOREFERER, true); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_TIMEOUT, 10); + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, null); $fp = fopen("example_homepage.txt", "w"); if ($fp === false) { @@ -56,6 +64,8 @@ public function allGood(string $url, array $header) { 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() { diff --git a/tests/PHPStan/Rules/Functions/data/function-call-param-closure-this.php b/tests/PHPStan/Rules/Functions/data/function-call-param-closure-this.php index 743103a5ac..0c4758a1d3 100644 --- a/tests/PHPStan/Rules/Functions/data/function-call-param-closure-this.php +++ b/tests/PHPStan/Rules/Functions/data/function-call-param-closure-this.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 += 7.4 + 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-closure-this-classes.php b/tests/PHPStan/Rules/Functions/data/param-closure-this-classes.php new file mode 100644 index 0000000000..d1652c132b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-closure-this-classes.php @@ -0,0 +1,27 @@ += 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/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/uasort_arrow.php b/tests/PHPStan/Rules/Functions/data/uasort_arrow.php index ad064939e7..c23a622d90 100644 --- a/tests/PHPStan/Rules/Functions/data/uasort_arrow.php +++ b/tests/PHPStan/Rules/Functions/data/uasort_arrow.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 += 7.4 +createReflectionProvider(), true, false, true, false, false, true, false), true); + return new YieldFromTypeRule(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true), true); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php index deec20a074..c35ec06136 100644 --- a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php @@ -14,7 +14,7 @@ class YieldTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new YieldTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false)); + return new YieldTypeRule(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true)); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php index eb34897e10..075c3fa6c2 100644 --- a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Generics; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -15,11 +16,12 @@ protected function getRule(): Rule { return new ClassAncestorsRule( new GenericAncestorsCheck( - $this->createReflectionProvider(), + self::createReflectionProvider(), new GenericObjectTypeCheck(), - new VarianceCheck(true, true), - true, + new VarianceCheck(), + new UnresolvableTypeHelper(), [], + true, ), new CrossCheckInterfacesHelper(), ); @@ -124,6 +126,10 @@ public function testRuleExtends(): void '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, + ], ]); } @@ -269,4 +275,14 @@ 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 788be4cbd5..27ed122f68 100644 --- a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php @@ -7,7 +7,7 @@ use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,7 +17,7 @@ class ClassTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new ClassTemplateTypeRule( @@ -26,6 +26,8 @@ protected function getRule(): Rule new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), new GenericObjectTypeCheck(), $typeAliasResolver, @@ -86,6 +88,18 @@ public function testRule(): void '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, + ], ]); } @@ -125,11 +139,9 @@ public function testInInterface(): void $this->analyse([__DIR__ . '/data/interface-template.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug10049(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } $this->analyse([__DIR__ . '/data/bug-10049.php'], [ [ 'PHPDoc tag @template for class Bug10049\SimpleEntity cannot have existing class Bug10049\SimpleEntity as its name.', diff --git a/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php index a7851f6c52..070c8e3021 100644 --- a/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php @@ -2,9 +2,10 @@ namespace PHPStan\Rules\Generics; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -16,22 +17,20 @@ protected function getRule(): Rule { return new EnumAncestorsRule( new GenericAncestorsCheck( - $this->createReflectionProvider(), + self::createReflectionProvider(), new GenericObjectTypeCheck(), - new VarianceCheck(true, true), - true, + new VarianceCheck(), + new UnresolvableTypeHelper(), [], + true, ), new CrossCheckInterfacesHelper(), ); } + #[RequiresPhp('>= 8.1')] public function testRule(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/enum-ancestors.php'], [ [ 'Enum EnumGenericAncestors\Foo has @implements tag, but does not implement any interface.', @@ -60,12 +59,9 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1')] public function testCrossCheckInterfaces(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/cross-check-interfaces-enums.php'], [ [ 'Interface IteratorAggregate specifies template type TValue of interface Traversable as string but it\'s already specified as CrossCheckInterfacesEnums\Item.', diff --git a/tests/PHPStan/Rules/Generics/EnumTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/EnumTemplateTypeRuleTest.php index dbd82cf985..6028064d26 100644 --- a/tests/PHPStan/Rules/Generics/EnumTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/EnumTemplateTypeRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,12 +17,9 @@ protected function getRule(): Rule return new EnumTemplateTypeRule(); } + #[RequiresPhp('>= 8.1')] public function testRule(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/enum-template.php'], [ [ 'Enum EnumTemplate\Foo has PHPDoc @template tag but enums cannot be generic.', diff --git a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php index 35df206865..f7a818012a 100644 --- a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php @@ -17,7 +17,7 @@ class FunctionTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new FunctionTemplateTypeRule( @@ -27,6 +27,8 @@ protected function getRule(): Rule new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), new GenericObjectTypeCheck(), $typeAliasResolver, @@ -67,6 +69,18 @@ public function testRule(): void '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 c57774efed..0fe5c98ba1 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Generics; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -15,11 +16,12 @@ protected function getRule(): Rule { return new InterfaceAncestorsRule( new GenericAncestorsCheck( - $this->createReflectionProvider(), + self::createReflectionProvider(), new GenericObjectTypeCheck(), - new VarianceCheck(true, true), - true, + new VarianceCheck(), + new UnresolvableTypeHelper(), [], + true, ), new CrossCheckInterfacesHelper(), ); diff --git a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php index 0623a7d1c2..e4c0da47f3 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php @@ -16,7 +16,7 @@ class InterfaceTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new InterfaceTemplateTypeRule( @@ -25,6 +25,8 @@ protected function getRule(): Rule new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), new GenericObjectTypeCheck(), $typeAliasResolver, @@ -65,6 +67,18 @@ public function testRule(): void 'Call-site variance of contravariant int in generic type InterfaceTemplateType\Covariant in PHPDoc tag @template W is in conflict with covariant template type T of interface InterfaceTemplateType\Covariant.', 74, ], + [ + 'PHPDoc tag @template T for interface InterfaceTemplateType\InvalidDefault has invalid default type InterfaceTemplateType\Zazzzu.', + 82, + ], + [ + 'Default type bool in PHPDoc tag @template T for interface InterfaceTemplateType\OutOfBoundsDefault is not subtype of bound type object.', + 90, + ], + [ + 'PHPDoc tag @template V for interface InterfaceTemplateType\RequiredAfterOptional does not have a default type but follows an optional @template U.', + 100, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php b/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php index 8f743f4dc7..81392060c2 100644 --- a/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -234,4 +235,15 @@ public function testPr2465(): void ]); } + #[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 index e754794566..c936422dce 100644 --- a/tests/PHPStan/Rules/Generics/MethodTagTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodTagTemplateTypeRuleTest.php @@ -17,20 +17,24 @@ class MethodTagTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new MethodTagTemplateTypeRule( - self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck( - $reflectionProvider, - new ClassNameCheck( - new ClassCaseSensitivityCheck($reflectionProvider, true), - new ClassForbiddenNameCheck(self::getContainer()), + new 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, ), - new GenericObjectTypeCheck(), - $typeAliasResolver, - true, ), ); } 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 a845993344..d8b9b5a909 100644 --- a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php @@ -17,7 +17,7 @@ class MethodTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new MethodTemplateTypeRule( @@ -27,6 +27,8 @@ protected function getRule(): Rule new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), new GenericObjectTypeCheck(), $typeAliasResolver, @@ -73,6 +75,18 @@ public function testRule(): void '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 index 97870f3572..8998d9712e 100644 --- a/tests/PHPStan/Rules/Generics/PropertyVarianceRuleTest.php +++ b/tests/PHPStan/Rules/Generics/PropertyVarianceRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -16,7 +16,6 @@ protected function getRule(): Rule { return new PropertyVarianceRule( self::getContainer()->getByType(VarianceCheck::class), - true, ); } @@ -58,12 +57,9 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.0')] public function testPromoted(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/property-variance-promoted.php'], [ [ 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$a.', @@ -100,12 +96,9 @@ public function testPromoted(): void ]); } + #[RequiresPhp('>= 8.1')] public function testReadOnly(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/property-variance-readonly.php'], [ [ 'Template type X is declared as covariant, but occurs in contravariant position in property PropertyVariance\ReadOnly\B::$b.', diff --git a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php index 99ad839123..f40914464b 100644 --- a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php @@ -17,7 +17,7 @@ class TraitTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new TraitTemplateTypeRule( @@ -27,6 +27,8 @@ protected function getRule(): Rule new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), new GenericObjectTypeCheck(), $typeAliasResolver, @@ -69,6 +71,18 @@ public function testRule(): void '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 2101cedb5c..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,12 @@ protected function getRule(): Rule return new UsedTraitsRule( self::getContainer()->getByType(FileTypeMapper::class), new GenericAncestorsCheck( - $this->createReflectionProvider(), + self::createReflectionProvider(), new GenericObjectTypeCheck(), - new VarianceCheck(true, true), - true, + new VarianceCheck(), + new UnresolvableTypeHelper(), [], + true, ), ); } 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-6301.php b/tests/PHPStan/Rules/Generics/data/bug-6301.php index 9811b63253..8d32598294 100644 --- a/tests/PHPStan/Rules/Generics/data/bug-6301.php +++ b/tests/PHPStan/Rules/Generics/data/bug-6301.php @@ -22,7 +22,7 @@ public function str($s) * @param literal-string $literalString */ public function foo(int $i, $nonEmpty, $numericString, $literalString):void { - assertType('numeric-string', $this->str((string) $i)); + assertType('lowercase-string&numeric-string&uppercase-string', $this->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/class-ancestors-extends.php b/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php index bb942838ed..c04a5665a4 100644 --- a/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php +++ b/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php @@ -247,3 +247,29 @@ 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 7a016c36ce..abbc514279 100644 --- a/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php +++ b/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php @@ -242,3 +242,18 @@ class FooCollection implements AbstractFooCollection 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 a76c2eeab0..06400bd536 100644 --- a/tests/PHPStan/Rules/Generics/data/class-template.php +++ b/tests/PHPStan/Rules/Generics/data/class-template.php @@ -114,3 +114,29 @@ class Adipiscing { } + +/** + * @template T = Zazzzu + */ +class Elit +{ + +} + +/** + * @template T of object = bool + */ +class Venenatis +{ + +} + +/** + * @template T + * @template U = string + * @template V + */ +class Mauris +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/enum-ancestors.php b/tests/PHPStan/Rules/Generics/data/enum-ancestors.php index e5d7848498..1cda1bcbcd 100644 --- a/tests/PHPStan/Rules/Generics/data/enum-ancestors.php +++ b/tests/PHPStan/Rules/Generics/data/enum-ancestors.php @@ -94,3 +94,16 @@ enum TypeProjection implements Generic { } + +/** + * @template T = string + */ +interface GenericDefault +{ + +} + +enum Foo9 implements GenericDefault +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/function-template.php b/tests/PHPStan/Rules/Generics/data/function-template.php index 8b9de2cdaf..8a1ff456f9 100644 --- a/tests/PHPStan/Rules/Generics/data/function-template.php +++ b/tests/PHPStan/Rules/Generics/data/function-template.php @@ -95,3 +95,29 @@ 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-template.php b/tests/PHPStan/Rules/Generics/data/interface-template.php index 8ae819c99b..7f0da436e7 100644 --- a/tests/PHPStan/Rules/Generics/data/interface-template.php +++ b/tests/PHPStan/Rules/Generics/data/interface-template.php @@ -75,3 +75,29 @@ 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-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 0fc67c1b24..edf5d62201 100644 --- a/tests/PHPStan/Rules/Generics/data/method-template.php +++ b/tests/PHPStan/Rules/Generics/data/method-template.php @@ -112,3 +112,34 @@ 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/trait-template.php b/tests/PHPStan/Rules/Generics/data/trait-template.php index 7c9e179295..39126a88f2 100644 --- a/tests/PHPStan/Rules/Generics/data/trait-template.php +++ b/tests/PHPStan/Rules/Generics/data/trait-template.php @@ -65,3 +65,29 @@ 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 f01c5e9dfb..f34fb5ffb9 100644 --- a/tests/PHPStan/Rules/Generics/data/used-traits.php +++ b/tests/PHPStan/Rules/Generics/data/used-traits.php @@ -69,3 +69,17 @@ class Dolor use GenericTrait; } + +/** + * @template T = string + */ +trait GenericDefault +{ +} + +class Sit +{ + + use GenericDefault; + +} diff --git a/tests/PHPStan/Rules/InternalTag/RestrictedInternalClassConstantUsageExtensionTest.php b/tests/PHPStan/Rules/InternalTag/RestrictedInternalClassConstantUsageExtensionTest.php new file mode 100644 index 0000000000..a9e89567ca --- /dev/null +++ b/tests/PHPStan/Rules/InternalTag/RestrictedInternalClassConstantUsageExtensionTest.php @@ -0,0 +1,90 @@ + + */ +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 @@ + @@ -46,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/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/Analyser/data/die-73.php b/tests/PHPStan/Rules/Keywords/data/bug-11738-included.php similarity index 58% rename from tests/PHPStan/Analyser/data/die-73.php rename to tests/PHPStan/Rules/Keywords/data/bug-11738-included.php index b1bce4861f..a4abe2dafc 100644 --- a/tests/PHPStan/Analyser/data/die-73.php +++ b/tests/PHPStan/Rules/Keywords/data/bug-11738-included.php @@ -1,3 +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/include-me-to-prove-you-work.txt b/tests/PHPStan/Rules/Keywords/data/include-me-to-prove-you-work.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/PHPStan/Rules/Keywords/data/require-file-conditionally.php b/tests/PHPStan/Rules/Keywords/data/require-file-conditionally.php new file mode 100644 index 0000000000..2d18f0066e --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/data/require-file-conditionally.php @@ -0,0 +1,12 @@ + @@ -45,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()); @@ -71,12 +71,9 @@ public function testNonAbstractMethodWithNoBody(): void ]); } + #[RequiresPhp('>= 8.1')] public function testEnum(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/method-in-enum-without-body.php'], [ [ 'Non-abstract method MethodInEnumWithoutBody\Foo::doFoo() must contain a body.', @@ -89,4 +86,31 @@ public function testEnum(): void ]); } + #[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/CallMethodsRuleNoBleedingEdgeTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleNoBleedingEdgeTest.php deleted file mode 100644 index 71678d99ff..0000000000 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleNoBleedingEdgeTest.php +++ /dev/null @@ -1,61 +0,0 @@ - - */ -class CallMethodsRuleNoBleedingEdgeTest extends RuleTestCase -{ - - private bool $checkExplicitMixed; - - protected function getRule(): Rule - { - $reflectionProvider = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, false, true, false); - return new CallMethodsRule( - new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), - new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion(PHP_VERSION_ID), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, false), - ); - } - - public function testGenericsInferCollection(): void - { - $this->checkExplicitMixed = true; - $this->analyse([__DIR__ . '/data/generics-infer-collection.php'], [ - [ - 'Parameter #1 $c of method GenericsInferCollection\Foo::doBar() expects GenericsInferCollection\ArrayCollection, GenericsInferCollection\ArrayCollection given.', - 43, - ], - ]); - } - - public function testGenericsInferCollectionLevel8(): void - { - $this->checkExplicitMixed = false; - $this->analyse([__DIR__ . '/data/generics-infer-collection.php'], [ - [ - 'Parameter #1 $c of method GenericsInferCollection\Foo::doBar() expects GenericsInferCollection\ArrayCollection, GenericsInferCollection\ArrayCollection given.', - 43, - ], - ]); - } - - public static function getAdditionalConfigFiles(): array - { - // no bleeding edge - return []; - } - -} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 8877e53e31..ff3ba64c16 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Methods; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -10,6 +9,8 @@ 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; /** @@ -28,36 +29,28 @@ class CallMethodsRuleTest extends RuleTestCase private bool $checkImplicitMixed = false; - private int $phpVersion = PHP_VERSION_ID; - protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, $this->checkNullables, $this->checkThisOnly, $this->checkUnionTypes, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false); + $reflectionProvider = self::createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, $this->checkNullables, $this->checkThisOnly, $this->checkUnionTypes, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true); return new CallMethodsRule( new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), - new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion($this->phpVersion), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), + new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), ); } + #[RequiresPhp('< 8.0')] public function testIsCallablePhp7(): void { - if (PHP_VERSION_ID >= 80000) { - $this->markTestSkipped('Test requires PHP 7.0'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; $this->analyse([ __DIR__ . '/data/call-methods-is-callable.php'], []); } + #[RequiresPhp('>= 8.0')] public function testIsCallablePhp8(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -457,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.', @@ -793,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.', @@ -1306,7 +1305,7 @@ public function testCallMethodsNullIssue(): void $this->analyse([__DIR__ . '/data/order.php'], []); } - public function dataIterable(): array + public static function dataIterable(): array { return [ [ @@ -1318,9 +1317,7 @@ public function dataIterable(): array ]; } - /** - * @dataProvider dataIterable - */ + #[DataProvider('dataIterable')] public function testIterables(bool $checkNullables): void { $this->checkThisOnly = false; @@ -1549,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 [ [ @@ -1590,9 +1587,10 @@ public function dataExplicitMixed(): array } /** - * @dataProvider dataExplicitMixed * @param list $errors */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataExplicitMixed')] public function testExplicitMixed(bool $checkExplicitMixed, array $errors): void { $this->checkThisOnly = false; @@ -1602,7 +1600,7 @@ public function testExplicitMixed(bool $checkExplicitMixed, array $errors): void $this->analyse([__DIR__ . '/data/check-explicit-mixed.php'], $errors); } - public function dataImplicitMixed(): array + public static function dataImplicitMixed(): array { return [ [ @@ -1630,9 +1628,9 @@ public function dataImplicitMixed(): array } /** - * @dataProvider dataImplicitMixed * @param list $errors */ + #[DataProvider('dataImplicitMixed')] public function testImplicitMixed(bool $checkImplicitMixed, array $errors): void { $this->checkThisOnly = false; @@ -1722,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; @@ -1794,12 +1789,9 @@ public function testNullSafe(): void ]); } + #[RequiresPhp('< 8.0')] public function testDisallowNamedArguments(): void { - if (PHP_VERSION_ID >= 80000) { - $this->markTestSkipped('Test requires PHP earlier than 8.0.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -1812,12 +1804,26 @@ public function testDisallowNamedArguments(): void ]); } + public function testDisallowNamedArgumentsInPhpVersionScope(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + $this->analyse([__DIR__ . '/data/disallow-named-arguments-php-version-scope.php'], [ + [ + 'Named arguments are supported only on PHP 8.0 and later.', + 26, + ], + ]); + } + + #[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'], [ [ @@ -1897,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, ], [ @@ -1912,6 +1918,14 @@ 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, + ], ]); } @@ -1945,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, ], [ @@ -2081,12 +2095,13 @@ public function testBug3546(): void $this->analyse([__DIR__ . '/data/bug-3546.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug4800(): void { $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().', @@ -2256,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, ], [ @@ -2327,12 +2342,9 @@ public function testBug3465(): void $this->analyse([__DIR__ . '/data/bug-3465.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug5868(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -2370,12 +2382,9 @@ public function testFirstClassCallable(): void $this->analyse([__DIR__ . '/data/first-class-method-callable.php'], []); } + #[RequiresPhp('>= 8.1')] public function testEnums(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs PHP 8.1'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -2420,12 +2429,9 @@ public function testEnums(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug6239(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('This test needs PHP 8.0'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -2453,12 +2459,9 @@ public function testRectorDoWhileVarIssue(): void ]); } + #[RequiresPhp('>= 8.1')] public function testReadOnlyPropertyPassedByReference(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -2501,12 +2504,9 @@ public function testBug6236(): void $this->analyse([__DIR__ . '/data/bug-6236.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug6118(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -2622,12 +2622,9 @@ public function testGenericVariance(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug6904(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -2684,12 +2681,9 @@ public function testConditionalComplexTemplates(): void $this->analyse([__DIR__ . '/data/conditional-complex-templates.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug6291(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -2706,6 +2700,7 @@ public function testBug1517(): void $this->analyse([__DIR__ . '/data/bug-1517.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug7593(): void { $this->checkThisOnly = false; @@ -2742,11 +2737,9 @@ public function testBug7600(): void $this->analyse([__DIR__ . '/data/bug-7600.php'], []); } + #[RequiresPhp('>= 8.2')] public function testBug8058(): void { - if (PHP_VERSION_ID < 80200) { - $this->markTestSkipped('Test requires PHP 8.2'); - } $this->checkThisOnly = false; $this->checkNullables = false; $this->checkUnionTypes = false; @@ -2754,11 +2747,9 @@ public function testBug8058(): void $this->analyse([__DIR__ . '/data/bug-8058.php'], []); } + #[RequiresPhp('< 8.2')] public function testBug8058b(): void { - if (PHP_VERSION_ID >= 80200) { - $this->markTestSkipped('Test requires PHP before 8.2'); - } $this->checkThisOnly = false; $this->checkNullables = false; $this->checkUnionTypes = false; @@ -2789,6 +2780,15 @@ public function testBug5623(): void $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; @@ -2841,7 +2841,7 @@ public function testBug8752(): void ]); } - public function dataCallablesWithoutCheckNullables(): iterable + public static function dataCallablesWithoutCheckNullables(): iterable { yield [false, false, []]; yield [true, false, []]; @@ -2881,9 +2881,9 @@ public function dataCallablesWithoutCheckNullables(): iterable } /** - * @dataProvider dataCallablesWithoutCheckNullables * @param list $expectedErrors */ + #[DataProvider('dataCallablesWithoutCheckNullables')] public function testCallablesWithoutCheckNullables(bool $checkNullables, bool $checkUnionTypes, array $expectedErrors): void { $this->checkThisOnly = false; @@ -2892,12 +2892,9 @@ public function testCallablesWithoutCheckNullables(bool $checkNullables, bool $c $this->analyse([__DIR__ . '/data/callables-without-check-nullables.php'], $expectedErrors); } + #[RequiresPhp('>= 8.0')] public function testBug8713(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -3003,89 +3000,95 @@ public function testObjectShapes(): void [ 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, Exception given.', 14, + PHP_VERSION_ID >= 80200 ? 'Exception does not have property $foo.' : 'Exception might not have property $foo.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, Exception given.', + 15, 'Exception might not have property $foo.', ], [ 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, object{foo: string, bar: int} given.', - 36, + 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.', - 37, + 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.', - 40, + 41, ], [ 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, object{foo: string, bar: int}&stdClass given.', - 43, + 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.', - 54, + 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.', - 55, + 56, ], [ 'Parameter #1 $bar of method ObjectShapesAcceptance\Bar::requireBar() expects ObjectShapesAcceptance\Bar, object{a: int} given.', - 71, + 72, ], [ 'Parameter #1 $o of method ObjectShapesAcceptance\Bar::doBar() expects object{a: string}, ObjectShapesAcceptance\Bar given.', - 77, + 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.', - 105, + 106, 'Property ObjectShapesAcceptance\Baz::$a is not public.', ], [ 'Parameter #1 $o of method ObjectShapesAcceptance\Baz::doBaz() expects object{b: int}, $this(ObjectShapesAcceptance\Baz) given.', - 106, + 107, 'Property ObjectShapesAcceptance\Baz::$b is static.', ], [ 'Parameter #1 $o of method ObjectShapesAcceptance\Baz::doLorem() expects object{c: int}, $this(ObjectShapesAcceptance\Baz) given.', - 107, + 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.', - 108, + 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.', - 156, + 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.', - 157, + 158, 'Property ($foo) type int does not accept type string.', ], [ 'Parameter #1 $o of method ObjectShapesAcceptance\TestAcceptance::doFoo() expects object{foo: int}, Traversable given.', - 209, + 210, 'Traversable might not have property $foo.', ], [ 'Parameter #1 $o of method ObjectShapesAcceptance\TestAcceptance::doFoo() expects object{foo: int}, ObjectShapesAcceptance\FinalClass given.', - 210, + 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; @@ -3104,12 +3107,9 @@ public function testBug9951(): void ]); } + #[RequiresPhp('>= 8.3')] public function testTypedClassConstants(): void { - if (PHP_VERSION_ID < 80300) { - $this->markTestSkipped('Test requires PHP 8.3.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -3117,12 +3117,9 @@ public function testTypedClassConstants(): void $this->analyse([__DIR__ . '/data/return-type-class-constant.php'], []); } + #[RequiresPhp('>= 8.0')] public function testNamedParametersForMultiVariantFunctions(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -3215,12 +3212,9 @@ public function testBug6371(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBugTemplateMixedUnionIntersect(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -3256,6 +3250,22 @@ public function testBug9009(): void $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; @@ -3322,4 +3332,328 @@ public function testClosureParameterGenerics(): void $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 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'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 27718dc9c7..282415af6f 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Methods; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -13,9 +12,10 @@ 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; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -31,8 +31,8 @@ class CallStaticMethodsRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false); + $reflectionProvider = self::createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true); return new CallStaticMethodsRule( new StaticMethodCallCheck( $reflectionProvider, @@ -40,21 +40,22 @@ protected function getRule(): Rule new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), true, true, + true, ), new FunctionCallParametersCheck( $ruleLevelHelper, new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, - true, ), ); } @@ -411,6 +412,7 @@ public function testBug2164(): void ]); } + #[RequiresPhp('>= 8.0')] public function testNamedArguments(): void { $this->checkThisOnly = false; @@ -448,12 +450,9 @@ public function testBug4550(): void ]); } + #[RequiresPhp('< 8.0')] public function testBug1971(): void { - if (PHP_VERSION_ID >= 80000) { - $this->markTestSkipped('Test requires PHP 7.x'); - } - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/bug-1971.php'], [ [ @@ -463,12 +462,9 @@ public function testBug1971(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug1971Php8(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/bug-1971.php'], [ [ @@ -547,15 +543,15 @@ public function testDiscussion7004(): void $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/discussion-7004.php'], [ [ - 'Parameter #1 $data of static method Discussion7004\Foo::fromArray1() expects array, array given.', + '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.', + '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.', + 'Parameter #1 $data of static method Discussion7004\Foo::fromArray3() expects array{newsletterName: string, subscriberCount: int}, array given.', 48, ], ]); @@ -670,7 +666,7 @@ public function testRequireImplements(): void ]); } - public function dataMixed(): array + public static function dataMixed(): array { $explicitOnlyErrors = [ [ @@ -753,9 +749,10 @@ public function dataMixed(): array } /** - * @dataProvider dataMixed * @param list $errors */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataMixed')] public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void { $this->checkThisOnly = false; @@ -764,12 +761,9 @@ public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, ar $this->analyse([__DIR__ . '/data/call-static-method-mixed.php'], $errors); } + #[RequiresPhp('>= 8.1')] public function testBugWrongMethodNameWithTemplateMixed(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1'); - } - $this->checkThisOnly = false; $this->checkExplicitMixed = true; $this->checkImplicitMixed = true; @@ -841,4 +835,68 @@ public function testClosureBind(): void ]); } + public function testBug10872(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-10872.php'], []); + } + + public function testBug12015(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12015.php'], []); + } + + public function testDynamicCall(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/dynamic-call.php'], [ + [ + 'Call to an undefined static method MethodsDynamicCall\Foo::bar().', + 33, + ], + [ + 'Call to an undefined static method MethodsDynamicCall\Foo::doBar().', + 36, + ], + [ + 'Call to an undefined static method MethodsDynamicCall\Foo::doBuz().', + 36, + ], + [ + 'Parameter #1 $n of method MethodsDynamicCall\Foo::doFoo() expects int, int|string given.', + 58, + ], + [ + 'Parameter #1 $s of static method MethodsDynamicCall\Foo::doQux() expects string, int given.', + 59, + ], + [ + 'Parameter #1 $n of method MethodsDynamicCall\Foo::doFoo() expects int, string given.', + 60, + ], + ]); + } + + public function testRestrictedInternalClassNameUsage(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/../InternalTag/data/static-method-call-on-internal-subclass.php'], [ + [ + 'Call to static method doFoo() on internal class StaticMethodCallOnInternalSubclassOne\Bar.', + 33, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php index 29e99526e2..8c7216ed4c 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(), true); + return new CallToConstructorStatementWithoutSideEffectsRule(self::createReflectionProvider()); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php index f1d3d5902d..a160bfcea7 100644 --- a/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -14,7 +16,7 @@ class CallToMethodStatementWithoutSideEffectsRuleTest extends RuleTestCase protected function getRule(): Rule { - return new CallToMethodStatementWithoutSideEffectsRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false)); + return new CallToMethodStatementWithoutSideEffectsRule(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true)); } public function testRule(): void @@ -89,6 +91,26 @@ public function testBug4455(): void $this->analyse([__DIR__ . '/data/bug-4455.php'], []); } + public function testBug11503(): void + { + $errors = [ + ['Call to method DateTimeImmutable::add() on a separate line has no effect.', 10], + ['Call to method DateTimeImmutable::modify() on a separate line has no effect.', 11], + ['Call to method DateTimeImmutable::setDate() on a separate line has no effect.', 12], + ['Call to method DateTimeImmutable::setISODate() on a separate line has no effect.', 13], + ['Call to method DateTimeImmutable::setTime() on a separate line has no effect.', 14], + ['Call to method DateTimeImmutable::setTimestamp() on a separate line has no effect.', 15], + ['Call to method DateTimeImmutable::setTimezone() on a separate line has no effect.', 17], + ]; + if (PHP_VERSION_ID < 80300) { + $errors = array_merge([ + ['Call to method DateTimeImmutable::sub() on a separate line has no effect.', 9], + ], $errors); + } + + $this->analyse([__DIR__ . '/data/bug-11503.php'], $errors); + } + public function testFirstClassCallables(): void { $this->analyse([__DIR__ . '/data/first-class-callable-method-without-side-effect.php'], [ diff --git a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php index 03cbdaa49d..bec56ff7d6 100644 --- a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php @@ -14,9 +14,9 @@ class CallToStaticMethodStatementWithoutSideEffectsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $broker = self::createReflectionProvider(); return new CallToStaticMethodStatementWithoutSideEffectsRule( - new RuleLevelHelper($broker, true, false, true, false, false, true, false), + new RuleLevelHelper($broker, true, false, true, false, false, false, true), $broker, ); } @@ -28,10 +28,6 @@ public function testRule(): void 'Call to static method DateTimeImmutable::createFromFormat() on a separate line has no effect.', 12, ], - [ - 'Call to static method DateTimeImmutable::createFromFormat() on a separate line has no effect.', - 13, - ], [ 'Call to method DateTime::format() on a separate line has no effect.', 23, diff --git a/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php b/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php index 4ea2484ef5..adcb69cdbe 100644 --- a/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php @@ -5,7 +5,6 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use function sprintf; -use const PHP_VERSION_ID; /** @extends RuleTestCase */ class ConsistentConstructorRuleTest extends RuleTestCase @@ -13,14 +12,17 @@ class ConsistentConstructorRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ConsistentConstructorRule(self::getContainer()->getByType(MethodParameterComparisonHelper::class)); + return new ConsistentConstructorRule( + self::getContainer()->getByType(MethodParameterComparisonHelper::class), + self::getContainer()->getByType(MethodVisibilityComparisonHelper::class), + ); } public function testRule(): void { $this->analyse([__DIR__ . '/data/consistent-constructor.php'], [ [ - sprintf('Parameter #1 $b (int) of method ConsistentConstructor\Bar2::__construct() is not %s with parameter #1 $b (string) of method ConsistentConstructor\Bar::__construct().', PHP_VERSION_ID >= 70400 ? 'contravariant' : 'compatible'), + 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, ], [ @@ -40,10 +42,17 @@ public function testRule(): void public function testRuleNoErrors(): void { - $this->analyse( - [__DIR__ . '/data/consistent-constructor-no-errors.php'], - PHP_VERSION_ID < 70400 ? [['Parameter #1 $b (ConsistentConstructorNoErrors\A) of method ConsistentConstructorNoErrors\Baz::__construct() is not compatible with parameter #1 $b (ConsistentConstructorNoErrors\B) of method ConsistentConstructorNoErrors\Foo::__construct().', 49]] : [], - ); + $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/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php index a0cf90c942..7342558234 100644 --- a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php @@ -10,6 +10,8 @@ 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; /** @@ -22,13 +24,15 @@ class ExistingClassesInTypehintsRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $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), @@ -172,7 +176,7 @@ public function testVoidParameterTypehint(): void ]); } - public function dataNativeUnionTypes(): array + public static function dataNativeUnionTypes(): array { return [ [ @@ -196,16 +200,16 @@ public function dataNativeUnionTypes(): array } /** - * @dataProvider dataNativeUnionTypes * @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 [ [ @@ -356,9 +360,10 @@ public function dataRequiredParameterAfterOptional(): array } /** - * @dataProvider dataRequiredParameterAfterOptional * @param list $errors */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataRequiredParameterAfterOptional')] public function testRequiredParameterAfterOptional(int $phpVersionId, array $errors): void { $this->phpVersionId = $phpVersionId; @@ -375,7 +380,7 @@ public function testBug4641(): void ]); } - public function dataIntersectionTypes(): array + public static function dataIntersectionTypes(): array { return [ [80000, []], @@ -404,9 +409,9 @@ public function dataIntersectionTypes(): array } /** - * @dataProvider dataIntersectionTypes * @param list $errors */ + #[DataProvider('dataIntersectionTypes')] public function testIntersectionTypes(int $phpVersion, array $errors): void { $this->phpVersionId = $phpVersion; @@ -414,12 +419,9 @@ public function testIntersectionTypes(int $phpVersion, array $errors): void $this->analyse([__DIR__ . '/data/intersection-types.php'], $errors); } + #[RequiresPhp('>= 8.1')] public function testEnums(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs PHP 8.1'); - } - $this->analyse([__DIR__ . '/data/enums-typehints.php'], [ [ 'Parameter $int of method EnumsTypehints\Foo::doFoo() has invalid type EnumsTypehints\intt.', @@ -428,24 +430,70 @@ public function testEnums(): void ]); } - public function dataTrueTypes(): array + public function testTrueTypehint(): void { - return [ - [80200, []], - ]; - } - - /** - * @dataProvider dataTrueTypes - * @param list $errors - */ - public function testTrueTypehint(int $phpVersion, array $errors): void - { - $this->phpVersionId = $phpVersion; + 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'], [ @@ -461,6 +509,7 @@ 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'], [ @@ -471,4 +520,83 @@ public function testTemplateInParamOut(): void ]); } + 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'], []); + } + } 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 index 64a4b89c0f..e9def238c3 100644 --- a/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php @@ -5,21 +5,19 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use const PHP_VERSION_ID; /** @extends RuleTestCase */ class FinalPrivateMethodRuleTest extends RuleTestCase { - private int $phpVersionId; - protected function getRule(): Rule { - return new FinalPrivateMethodRule( - new PhpVersion($this->phpVersionId), - ); + return new FinalPrivateMethodRule(); } - public function dataRule(): array + public static function dataRule(): array { return [ [ @@ -39,13 +37,40 @@ public function dataRule(): array } /** - * @dataProvider dataRule * @param list $errors */ + #[DataProvider('dataRule')] public function testRule(int $phpVersion, array $errors): void { - $this->phpVersionId = $phpVersion; + $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/IllegalConstructorMethodCallRuleTest.php b/tests/PHPStan/Rules/Methods/IllegalConstructorMethodCallRuleTest.php deleted file mode 100644 index 8b0f957e85..0000000000 --- a/tests/PHPStan/Rules/Methods/IllegalConstructorMethodCallRuleTest.php +++ /dev/null @@ -1,37 +0,0 @@ - - */ -class IllegalConstructorMethodCallRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new IllegalConstructorMethodCallRule(); - } - - public function testMethods(): void - { - $this->analyse([__DIR__ . '/data/illegal-constructor-call-rule-test.php'], [ - [ - 'Call to __construct() on an existing object is not allowed.', - 13, - ], - [ - 'Call to __construct() on an existing object is not allowed.', - 18, - ], - [ - 'Call to __construct() on an existing object is not allowed.', - 60, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Methods/IllegalConstructorStaticCallRuleTest.php b/tests/PHPStan/Rules/Methods/IllegalConstructorStaticCallRuleTest.php deleted file mode 100644 index 065a5785bf..0000000000 --- a/tests/PHPStan/Rules/Methods/IllegalConstructorStaticCallRuleTest.php +++ /dev/null @@ -1,59 +0,0 @@ - - */ -class IllegalConstructorStaticCallRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new IllegalConstructorStaticCallRule(); - } - - public function testMethods(): void - { - $this->analyse([__DIR__ . '/data/illegal-constructor-call-rule-test.php'], [ - [ - 'Static call to __construct() is only allowed on a parent class in the constructor.', - 31, - ], - [ - 'Static call to __construct() is only allowed on a parent class in the constructor.', - 43, - ], - [ - 'Static call to __construct() is only allowed on a parent class in the constructor.', - 44, - ], - [ - 'Static call to __construct() is only allowed on a parent class in the constructor.', - 49, - ], - [ - 'Static call to __construct() is only allowed on a parent class in the constructor.', - 50, - ], - [ - 'Static call to __construct() is only allowed on a parent class in the constructor.', - 100, - ], - ]); - } - - public function testBug9577(): void - { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - - $this->analyse([__DIR__ . '/data/bug-9577.php'], []); - } - -} diff --git a/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php b/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php index e55eb6eede..df7e897d77 100644 --- a/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -73,6 +74,7 @@ public function testDefaultValueForPromotedProperty(): void ]); } + #[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 9ce75af6e6..b495431f7c 100644 --- a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Methods; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -21,29 +20,27 @@ class MethodAttributesRuleTest extends RuleTestCase { - private int $phpVersion; - protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new MethodAttributesRule( new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), - new PhpVersion($this->phpVersion), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, - true, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, false), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), true, ), @@ -52,8 +49,6 @@ protected function getRule(): Rule public function testRule(): void { - $this->phpVersion = 80000; - $this->analyse([__DIR__ . '/data/method-attributes.php'], [ [ 'Attribute class MethodAttributes\Foo does not have the method target.', @@ -64,7 +59,6 @@ public function testRule(): void public function testBug5898(): void { - $this->phpVersion = 70400; $this->analyse([__DIR__ . '/data/bug-5898.php'], []); } diff --git a/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php index b5bc1f8efb..6dcf45e242 100644 --- a/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use const PHP_VERSION_ID; /** @@ -18,8 +19,8 @@ class MethodCallableRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false); + $reflectionProvider = self::createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true); return new MethodCallableRule( new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), @@ -27,11 +28,9 @@ protected function getRule(): Rule ); } + #[RequiresPhp('< 8.1')] public function testNotSupportedOnOlderVersions(): void { - if (PHP_VERSION_ID >= 80100) { - self::markTestSkipped('Test runs on PHP < 8.1.'); - } $this->analyse([__DIR__ . '/data/method-callable-not-supported.php'], [ [ 'First-class callables are supported only on PHP 8.1 and later.', @@ -40,12 +39,9 @@ public function testNotSupportedOnOlderVersions(): void ]); } + #[RequiresPhp('>= 8.1')] public function testRule(): void { - if (PHP_VERSION_ID < 80100) { - self::markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/method-callable.php'], [ [ 'Call to method MethodCallable\Foo::doFoo() with incorrect case: dofoo', diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index 93c6a67b9d..b23da34309 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Reflection\Php\PhpClassReflectionExtension; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use const PHP_VERSION_ID; /** @@ -26,12 +27,11 @@ protected function getRule(): Rule return new OverridingMethodRule( $phpVersion, - new MethodSignatureRule($phpClassReflectionExtension, $this->reportMaybes, $this->reportStatic, true), - true, - new MethodParameterComparisonHelper($phpVersion, true), - $phpClassReflectionExtension, - true, + new MethodSignatureRule($phpClassReflectionExtension, $this->reportMaybes, $this->reportStatic), true, + new MethodParameterComparisonHelper($phpVersion), + new MethodVisibilityComparisonHelper(), + new MethodPrototypeFinder($phpVersion, $phpClassReflectionExtension), false, ); } @@ -252,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'], []); @@ -381,12 +378,9 @@ public function testOverridenMethodWithConditionalReturnType(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug7652(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->reportMaybes = true; $this->reportStatic = true; $this->analyse([__DIR__ . '/data/bug-7652.php'], [ @@ -456,12 +450,9 @@ public function testBug9905(): void $this->analyse([__DIR__ . '/data/bug-9905.php'], []); } + #[RequiresPhp('>= 8.0')] public function testTraits(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->reportMaybes = true; $this->reportStatic = true; @@ -473,12 +464,9 @@ public function testTraits(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug10166(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->reportMaybes = true; $this->reportStatic = true; @@ -490,12 +478,9 @@ public function testBug10166(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug10184(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->reportMaybes = true; $this->reportStatic = true; @@ -504,10 +489,6 @@ public function testBug10184(): void public function testBug10208(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->reportMaybes = true; $this->reportStatic = true; @@ -516,10 +497,6 @@ public function testBug10208(): void public function testBug6462(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->reportMaybes = true; $this->reportStatic = true; @@ -528,10 +505,6 @@ public function testBug6462(): void public function testBug4396(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->reportMaybes = true; $this->reportStatic = true; @@ -540,14 +513,49 @@ public function testBug4396(): void public function testBug3580(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->reportMaybes = true; $this->reportStatic = true; $this->analyse([__DIR__ . '/data/bug-3580.php'], []); } + #[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/MissingMagicSerializationMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMagicSerializationMethodsRuleTest.php index 989d8e0a52..ad3c23c992 100644 --- a/tests/PHPStan/Rules/Methods/MissingMagicSerializationMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMagicSerializationMethodsRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use const PHP_VERSION_ID; /** @@ -18,12 +19,9 @@ protected function getRule(): Rule return new MissingMagicSerializationMethodsRule(new PhpVersion(PHP_VERSION_ID)); } + #[RequiresPhp('>= 8.1')] public function testRule(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/missing-serialization.php'], [ [ 'Non-abstract class MissingMagicSerializationMethods\myObj implements the Serializable interface, but does not implement __serialize().', diff --git a/tests/PHPStan/Rules/Methods/MissingMethodImplementationRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodImplementationRuleTest.php index 10615f2928..ff51b465e8 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodImplementationRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodImplementationRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -29,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, ], ]); @@ -45,12 +45,9 @@ public function testBug3958(): void $this->analyse([__DIR__ . '/data/bug-3958.php'], []); } + #[RequiresPhp('>= 8.1')] public function testEnums(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/missing-method-impl-enum.php'], [ [ 'Enum MissingMethodImplEnum\Bar contains abstract method doFoo() from interface MissingMethodImplEnum\FooInterface.', @@ -59,4 +56,9 @@ public function testEnums(): void ]); } + public function testBug11665(): void + { + $this->analyse([__DIR__ . '/data/bug-11665.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php index fecb8a1045..fce941c3ce 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php @@ -14,7 +14,7 @@ class MissingMethodParameterTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - return new MissingMethodParameterTypehintRule(new MissingTypehintCheck(true, true, true, true, []), true); + return new MissingMethodParameterTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -82,6 +82,10 @@ public function testRule(): void '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); diff --git a/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php index 0762900901..dc0f1b7931 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -14,7 +15,7 @@ class MissingMethodReturnTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - return new MissingMethodReturnTypehintRule(new MissingTypehintCheck(true, true, true, true, [])); + return new MissingMethodReturnTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -53,6 +54,10 @@ public function testRule(): void '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, + ], ]); } @@ -96,4 +101,21 @@ 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'], []); + } + } 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 b954794c6e..ff7b48186a 100644 --- a/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -37,22 +37,31 @@ public function testBug8664(): void $this->analyse([__DIR__ . '/../../Analyser/data/bug-8664.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug9293(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9293.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug6922b(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/bug-6922b.php'], []); } + public function testBug8523(): void + { + $this->analyse([__DIR__ . '/data/bug-8523.php'], []); + } + + public function testBug8523b(): void + { + $this->analyse([__DIR__ . '/data/bug-8523b.php'], []); + } + + public function testBug8523c(): void + { + $this->analyse([__DIR__ . '/data/bug-8523c.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php index 826586689a..cb2171e112 100644 --- a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php @@ -6,6 +6,8 @@ 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; @@ -28,17 +30,16 @@ protected function getRule(): Rule return new OverridingMethodRule( $phpVersion, - new MethodSignatureRule($phpClassReflectionExtension, true, true, true), + new MethodSignatureRule($phpClassReflectionExtension, true, true), false, - new MethodParameterComparisonHelper($phpVersion, true), - $phpClassReflectionExtension, - true, - true, + new MethodParameterComparisonHelper($phpVersion), + new MethodVisibilityComparisonHelper(), + new MethodPrototypeFinder($phpVersion, $phpClassReflectionExtension), $this->checkMissingOverrideMethodAttribute, ); } - public function dataOverridingFinalMethod(): array + public static function dataOverridingFinalMethod(): array { return [ [ @@ -54,9 +55,7 @@ public function dataOverridingFinalMethod(): array ]; } - /** - * @dataProvider dataOverridingFinalMethod - */ + #[DataProvider('dataOverridingFinalMethod')] public function testOverridingFinalMethod(int $phpVersion, string $contravariantMessage): void { $errors = [ @@ -146,7 +145,7 @@ public function testOverridingFinalMethod(int $phpVersion, string $contravariant $this->analyse([__DIR__ . '/data/overriding-method.php'], $errors); } - public function dataParameterContravariance(): array + public static function dataParameterContravariance(): array { return [ [ @@ -229,9 +228,10 @@ public function dataParameterContravariance(): array } /** - * @dataProvider dataParameterContravariance * @param list $expectedErrors */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataParameterContravariance')] public function testParameterContravariance( string $file, int $phpVersion, @@ -242,7 +242,7 @@ public function testParameterContravariance( $this->analyse([$file], $expectedErrors); } - public function dataReturnTypeCovariance(): array + public static function dataReturnTypeCovariance(): array { return [ [ @@ -287,9 +287,9 @@ public function dataReturnTypeCovariance(): array } /** - * @dataProvider dataReturnTypeCovariance * @param list $expectedErrors */ + #[DataProvider('dataReturnTypeCovariance')] public function testReturnTypeCovariance( int $phpVersion, array $expectedErrors, @@ -299,9 +299,7 @@ public function testReturnTypeCovariance( $this->analyse([__DIR__ . '/data/return-type-covariance.php'], $expectedErrors); } - /** - * @dataProvider dataOverridingFinalMethod - */ + #[DataProvider('dataOverridingFinalMethod')] public function testParle(int $phpVersion, string $contravariantMessage, string $covariantMessage): void { $this->phpVersionId = $phpVersion; @@ -323,9 +321,7 @@ public function testVariadicParameterIsAlwaysOptional(): void $this->analyse([__DIR__ . '/data/variadic-always-optional.php'], []); } - /** - * @dataProvider dataOverridingFinalMethod - */ + #[DataProvider('dataOverridingFinalMethod')] public function testBug3403(int $phpVersion): void { $this->phpVersionId = $phpVersion; @@ -375,7 +371,7 @@ public function testVariadics(): void $this->analyse([__DIR__ . '/data/overriding-variadics.php'], $errors); } - public function dataLessOverridenParametersWithVariadic(): array + public static function dataLessOverridenParametersWithVariadic(): array { return [ [ @@ -432,16 +428,16 @@ public function dataLessOverridenParametersWithVariadic(): array } /** - * @dataProvider dataLessOverridenParametersWithVariadic * @param list $errors */ + #[DataProvider('dataLessOverridenParametersWithVariadic')] public function testLessOverridenParametersWithVariadic(int $phpVersionId, array $errors): void { $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/less-parameters-variadics.php'], $errors); } - public function dataParameterTypeWidening(): array + public static function dataParameterTypeWidening(): array { return [ [ @@ -461,9 +457,9 @@ public function dataParameterTypeWidening(): array } /** - * @dataProvider dataParameterTypeWidening * @param list $errors */ + #[DataProvider('dataParameterTypeWidening')] public function testParameterTypeWidening(int $phpVersionId, array $errors): void { $this->phpVersionId = $phpVersionId; @@ -476,7 +472,7 @@ public function testBug4516(): void $this->analyse([__DIR__ . '/data/bug-4516.php'], []); } - public function dataTentativeReturnTypes(): array + public static function dataTentativeReturnTypes(): array { return [ [70400, []], @@ -525,9 +521,9 @@ public function dataTentativeReturnTypes(): array } /** - * @dataProvider dataTentativeReturnTypes * @param list $errors */ + #[DataProvider('dataTentativeReturnTypes')] public function testTentativeReturnTypes(int $phpVersionId, array $errors): void { if (PHP_VERSION_ID < 80100) { @@ -555,18 +551,12 @@ public function testBug6264(): void public function testBug7717(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->phpVersionId = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/bug-7717.php'], []); } public function testBug6104(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->phpVersionId = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/bug-6104.php'], []); } @@ -577,12 +567,9 @@ public function testBug9391(): void $this->analyse([__DIR__ . '/data/bug-9391.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBugWithIndirectPrototype(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->phpVersionId = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/overriding-indirect-prototype.php'], [ [ @@ -620,10 +607,6 @@ public function testBug7859(): void public function testBug8081(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->phpVersionId = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/bug-8081.php'], [ [ @@ -635,10 +618,6 @@ public function testBug8081(): void public function testBug8500(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->phpVersionId = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/bug-8500.php'], [ [ @@ -650,10 +629,6 @@ public function testBug8500(): void public function testBug9014(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->phpVersionId = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/bug-9014.php'], [ [ @@ -680,10 +655,6 @@ public function testBug9135(): void public function testBug10101(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->phpVersionId = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/bug-10101.php'], [ [ @@ -693,12 +664,9 @@ public function testBug10101(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug9615(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $tipText = 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.'; $this->phpVersionId = PHP_VERSION_ID; @@ -758,12 +726,9 @@ public function testTraits(): void $this->analyse([__DIR__ . '/data/overriding-trait-methods.php'], $errors); } + #[RequiresPhp('>= 8.3')] public function testOverrideAttribute(): void { - if (PHP_VERSION_ID < 80300) { - $this->markTestSkipped('Test requires PHP 8.3.'); - } - $this->phpVersionId = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/override-attribute.php'], [ [ @@ -777,7 +742,7 @@ public function testOverrideAttribute(): void ]); } - public function dataCheckMissingOverrideAttribute(): iterable + public static function dataCheckMissingOverrideAttribute(): iterable { yield [false, 80000, []]; yield [true, 80000, []]; @@ -795,9 +760,9 @@ public function dataCheckMissingOverrideAttribute(): iterable } /** - * @dataProvider dataCheckMissingOverrideAttribute * @param list $errors */ + #[DataProvider('dataCheckMissingOverrideAttribute')] public function testCheckMissingOverrideAttribute(bool $checkMissingOverrideMethodAttribute, int $phpVersionId, array $errors): void { $this->checkMissingOverrideMethodAttribute = $checkMissingOverrideMethodAttribute; @@ -820,10 +785,47 @@ public function testBug10153(): void $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 6303bdcdc4..5da8bfa7b0 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -6,6 +6,8 @@ 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; /** @@ -22,7 +24,7 @@ class ReturnTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnionTypes, $this->checkExplicitMixed, false, true, $this->checkBenevolentUnionTypes))); + return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper(self::createReflectionProvider(), true, false, $this->checkUnionTypes, $this->checkExplicitMixed, false, $this->checkBenevolentUnionTypes, true))); } public function testReturnTypeRule(): void @@ -237,52 +239,72 @@ public function testReturnTypeRule(): void 759, ], [ - 'Method ReturnTypes\ArrayFillKeysIssue::getIPs2() should return array> but returns array>.', + 'Method ReturnTypes\ArrayFillKeysIssue::getIPs2() should return array> but returns array>.', 817, ], [ 'Method ReturnTypes\AssertThisInstanceOf::doBar() should return $this(ReturnTypes\AssertThisInstanceOf) but returns ReturnTypes\AssertThisInstanceOf&ReturnTypes\FooInterface.', - 840, + 839, ], [ - 'Method ReturnTypes\NestedArrayCheck::doFoo() should return array but returns array>.', - 860, + 'Method ReturnTypes\NestedArrayCheck::doFoo() should return array but returns array>.', + 859, ], [ 'Method ReturnTypes\NestedArrayCheck::doBar() should return array but returns array>.', - 875, + 874, ], [ 'Method ReturnTypes\Foo2::returnIntFromParent() should return int but returns string.', - 950, + 949, ], [ 'Method ReturnTypes\Foo2::returnIntFromParent() should return int but returns ReturnTypes\integer.', - 953, + 952, ], [ 'Method ReturnTypes\VariableOverwrittenInForeach::doFoo() should return int but returns int|string.', - 1011, + 1010, ], [ 'Method ReturnTypes\VariableOverwrittenInForeach::doBar() should return int but returns int|string.', - 1026, + 1025, ], [ 'Method ReturnTypes\ReturnStaticGeneric::instanceReturnsStatic() should return static(ReturnTypes\ReturnStaticGeneric) but returns ReturnTypes\ReturnStaticGeneric.', - 1066, + 1065, ], [ 'Method ReturnTypes\NeverReturn::doFoo() should never return but return statement found.', - 1241, + 1240, ], [ 'Method ReturnTypes\NeverReturn::doBaz3() should never return but return statement found.', - 1254, + 1253, ], ]); } + public function testMisleadingMixedType(): void + { + if (PHP_VERSION_ID >= 80000) { + $errors = []; + } else { + $errors = [ + [ + 'Method MethodMisleadingMixedReturn\Foo::misleadingMixedReturnType() should return MethodMisleadingMixedReturn\mixed but returns int.', + 11, + ], + [ + 'Method MethodMisleadingMixedReturn\Foo::misleadingMixedReturnType() should return MethodMisleadingMixedReturn\mixed but returns true.', + 14, + ], + ]; + } + $this->analyse([__DIR__ . '/data/method-misleading-mixed-return.php'], $errors); + } + + #[RequiresPhp('>= 8.0')] public function testMisleadingTypehintsInClassWithoutNamespace(): void { $this->analyse([__DIR__ . '/data/misleadingTypehints.php'], [ @@ -437,19 +459,24 @@ public function testInferArrayKey(): void public function testBug4590(): void { $this->analyse([__DIR__ . '/data/bug-4590.php'], [ + [ + 'Method Bug4590\OkResponse::testGenericStatic() should return static(Bug4590\OkResponse>) but returns static(Bug4590\OkResponse).', + 36, + 'Template type T on class Bug4590\OkResponse is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + ], [ 'Method Bug4590\\Controller::test1() should return Bug4590\\OkResponse> but returns Bug4590\\OkResponse.', - 39, + 47, 'Template type T on class Bug4590\OkResponse is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], [ 'Method Bug4590\\Controller::test2() should return Bug4590\\OkResponse> but returns Bug4590\\OkResponse.', - 47, + 55, 'Template type T on class Bug4590\OkResponse is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], [ 'Method Bug4590\\Controller::test3() should return Bug4590\\OkResponse> but returns Bug4590\\OkResponse.', - 55, + 63, 'Template type T on class Bug4590\OkResponse is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], ]); @@ -520,6 +547,7 @@ public function testBug2573(): void $this->analyse([__DIR__ . '/data/bug-2573-return.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug4603(): void { $this->analyse([__DIR__ . '/data/bug-4603.php'], []); @@ -540,7 +568,7 @@ public function testTemplateUnion(): void ]); } - public function dataBug5218(): array + public static function dataBug5218(): array { return [ [ @@ -560,9 +588,9 @@ public function dataBug5218(): array } /** - * @dataProvider dataBug5218 * @param list $errors */ + #[DataProvider('dataBug5218')] public function testBug5218(bool $checkExplicitMixed, array $errors): void { $this->checkExplicitMixed = $checkExplicitMixed; @@ -744,12 +772,9 @@ public function testTaggedUnions(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug7904(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/bug-7904.php'], []); } @@ -772,6 +797,7 @@ public function testBug6358(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug8071(): void { $this->checkExplicitMixed = true; @@ -834,7 +860,7 @@ 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}}, ...}, ...}.", + "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}.", ], @@ -846,6 +872,16 @@ 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'], []); @@ -870,7 +906,7 @@ public function testMagicSerialization(): void { $this->analyse([__DIR__ . '/data/magic-serialization.php'], [ [ - 'Method MagicSerialization\WrongSignature::__serialize() should return array but returns string.', + 'Method MagicSerialization\WrongSignature::__serialize() should return array but returns string.', 23, ], [ @@ -897,7 +933,7 @@ public function testMagicSignatures(): void 43, ], [ - 'Method MagicSignatures\WrongSignature::__debugInfo() should return array|null but returns string.', + 'Method MagicSignatures\WrongSignature::__debugInfo() should return array|null but returns string.', 47, ], [ @@ -1002,10 +1038,6 @@ public function testWrongListTip(): void public function testArrowFunctionReturningVoidClosure(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/arrow-function-returning-void-closure.php'], []); } @@ -1044,4 +1076,187 @@ 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, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php index c726ab7fdb..2511b87083 100644 --- a/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php +++ b/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php @@ -9,6 +9,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use const PHP_VERSION_ID; /** @@ -21,8 +22,8 @@ class StaticMethodCallableRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false); + $reflectionProvider = self::createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true); return new StaticMethodCallableRule( new StaticMethodCallCheck( @@ -31,20 +32,20 @@ protected function getRule(): Rule 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 { - if (PHP_VERSION_ID >= 80100) { - self::markTestSkipped('Test runs on PHP < 8.1.'); - } - $this->analyse([__DIR__ . '/data/static-method-callable-not-supported.php'], [ [ 'First-class callables are supported only on PHP 8.1 and later.', @@ -53,12 +54,9 @@ public function testNotSupportedOnOlderVersions(): void ]); } + #[RequiresPhp('>= 8.1')] public function testRule(): void { - if (PHP_VERSION_ID < 80100) { - self::markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/static-method-callable.php'], [ [ 'Call to static method StaticMethodCallable\Foo::doFoo() with incorrect case: dofoo', 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 +someMethod()->methodFromChild(); +}; 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-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-10872.php b/tests/PHPStan/Rules/Methods/data/bug-10872.php new file mode 100644 index 0000000000..9b1e68ce17 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10872.php @@ -0,0 +1,42 @@ + + */ + public function getRow(): array + { + return []; + } + + /** + * @template ExpectedType of array + * @param ExpectedType $expected + * @param array $actual + * @psalm-assert =ExpectedType $actual + */ + public static function assertSame(array $expected, array $actual): void + { + if ($actual !== $expected) { + throw new \Exception(); + } + } + + public function testEscapeIdentifier(): void + { + $names = [ + 'foo', + '2', + ]; + + $expected = array_combine($names, array_fill(0, count($names), 'x')); + + self::assertSame( + $expected, + $this->getRow() + ); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-11337.php b/tests/PHPStan/Rules/Methods/data/bug-11337.php new file mode 100644 index 0000000000..49dce6d7a8 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11337.php @@ -0,0 +1,72 @@ += 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-11503.php b/tests/PHPStan/Rules/Methods/data/bug-11503.php new file mode 100644 index 0000000000..d37f48cf07 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11503.php @@ -0,0 +1,19 @@ +sub($interval); + $date->add($interval); + $date->modify('+1 day'); + $date->setDate(2024, 8, 13); + $date->setISODate(2024, 1); + $date->setTime(0, 0, 0, 0); + $date->setTimestamp(1); + $zone = new \DateTimeZone('UTC'); + $date->setTimezone($zone); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-11559c.php b/tests/PHPStan/Rules/Methods/data/bug-11559c.php new file mode 100644 index 0000000000..54e3ba27b0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11559c.php @@ -0,0 +1,16 @@ +implicit_variadic_fn(1, 2, 3); + $c->regular_fn(1, 2, 3); +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-11663.php b/tests/PHPStan/Rules/Methods/data/bug-11663.php new file mode 100644 index 0000000000..ac2ed03a93 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11663.php @@ -0,0 +1,79 @@ +where('test'); + } + + /** + * @param __benevolent $template + * @return __benevolent + */ + public function test2($template) + { + return $template->where('test'); + } + + + /** + * @template T of A|B + * @param T $ab + * @return T + */ + function foo(A|B $ab): A|B + { + return $ab->doFoo(); + } + + /** + * @template T of __benevolent + * @param T $ab + * @return T + */ + function foo2(A|B $ab): A|B + { + return $ab->doFoo(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-11665.php b/tests/PHPStan/Rules/Methods/data/bug-11665.php new file mode 100644 index 0000000000..1926a10ac5 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11665.php @@ -0,0 +1,7 @@ += 8.0 + +namespace Bug11857Builder; + +class Foo +{ + + /** + * @param array $attributes + * @return $this + */ + public function filter(array $attributes): static + { + return $this; + } + + /** + * @param array $attributes + * @return $this + */ + public function filterUsingRequest(array $attributes): static + { + return $this->filter($attributes); + } + +} + +final class FinalFoo +{ + + /** + * @param array $attributes + * @return $this + */ + public function filter(array $attributes): static + { + return $this; + } + + /** + * @param array $attributes + * @return $this + */ + public function filterUsingRequest(array $attributes): static + { + return $this->filter($attributes); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-12015.php b/tests/PHPStan/Rules/Methods/data/bug-12015.php new file mode 100644 index 0000000000..c2a5618cd8 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12015.php @@ -0,0 +1,21 @@ + $field + */ + abstract public function field(array $field): static; +} + +class GroupBuilder +{ + use HasFieldBuildersTrait; + + /** @var array */ + private array $group = []; + + private function __construct() + { + } + + /** + * @param array $field + */ + public function field(array $field): static + { + if (! is_array($this->group['fields'] ?? null)) { + $this->group['fields'] = []; + } + + $this->group['fields'][] = $field; + + return $this; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-12137.php b/tests/PHPStan/Rules/Methods/data/bug-12137.php new file mode 100755 index 0000000000..eacb78bbf3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12137.php @@ -0,0 +1,23 @@ + + */ + public function sayHello(): array + { + $a = [1 => 'foo', 3 => 'bar', 5 => 'baz']; + return array_map(static fn(string $s, int $i): string => $s . $i, $a, array_keys($a)); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-12422.php b/tests/PHPStan/Rules/Methods/data/bug-12422.php new file mode 100644 index 0000000000..19ae6e4e6a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12422.php @@ -0,0 +1,28 @@ += 8.1 + +namespace Bug12422; + +enum MyEnum +{ + case A; + case B; +} + +class MyClass +{ + public function fooo(): void + { + } +} + +function test(MyEnum $enum, ?MyClass $bar): void +{ + if ($enum === MyEnum::A && $bar === null) { + return; + } + + match ($enum) { + MyEnum::A => $bar->fooo(), + MyEnum::B => null, + }; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-12471.php b/tests/PHPStan/Rules/Methods/data/bug-12471.php new file mode 100644 index 0000000000..fd242cc639 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12471.php @@ -0,0 +1,40 @@ += 8.4 + +declare(strict_types = 1); + +namespace Bug12501; + +final readonly class EmptyObject { + public function __construct( + public null $value1 = null, + ) {} +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-12544.php b/tests/PHPStan/Rules/Methods/data/bug-12544.php new file mode 100644 index 0000000000..56860b669d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12544.php @@ -0,0 +1,21 @@ +hello(); + $bar->somethingElse(); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-12739.php b/tests/PHPStan/Rules/Methods/data/bug-12739.php new file mode 100644 index 0000000000..646910cf3e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12739.php @@ -0,0 +1,58 @@ +> + */ + 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-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-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-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-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 + + */ + 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-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 + */ + 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-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-5372.php b/tests/PHPStan/Rules/Methods/data/bug-5372.php index 34d339385e..712516416c 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-5372.php +++ b/tests/PHPStan/Rules/Methods/data/bug-5372.php @@ -1,4 +1,4 @@ -= 7.4 +flush(); + $manager->flush(''); + $manager->flush('', ''); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6023.php b/tests/PHPStan/Rules/Methods/data/bug-6023.php index d2eb85eac2..59628e4bac 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-6023.php +++ b/tests/PHPStan/Rules/Methods/data/bug-6023.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 += 7.4 += 7.4 += 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-7717.php b/tests/PHPStan/Rules/Methods/data/bug-7717.php index 055fb03be9..791ec0453d 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-7717.php +++ b/tests/PHPStan/Rules/Methods/data/bug-7717.php @@ -1,4 +1,4 @@ -= 7.4 + $page], + $perPage !== null ? ['perPage' => $perPage] : [] + ); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8523.php b/tests/PHPStan/Rules/Methods/data/bug-8523.php new file mode 100644 index 0000000000..0cc8b3ad0a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8523.php @@ -0,0 +1,36 @@ +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-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-9141.php b/tests/PHPStan/Rules/Methods/data/bug-9141.php new file mode 100644 index 0000000000..bfe85c0658 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9141.php @@ -0,0 +1,26 @@ +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-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-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 @@ += 8.1 - -namespace Bug9577IllegalConstructorStaticCall; - -trait StringableMessageTrait -{ - public function __construct( - private readonly \Stringable $StringableMessage, - int $code = 0, - ?\Throwable $previous = null, - ) { - parent::__construct((string) $StringableMessage, $code, $previous); - } - - public function getStringableMessage(): \Stringable - { - return $this->StringableMessage; - } -} - -class SpecializedException extends \RuntimeException -{ - use StringableMessageTrait { - StringableMessageTrait::__construct as __traitConstruct; - } - - public function __construct( - private readonly object $aService, - \Stringable $StringableMessage, - int $code = 0, - ?\Throwable $previous = null, - ) { - $this->__traitConstruct($StringableMessage, $code, $previous); - } - - public function getService(): object - { - return $this->aService; - } -} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9657.php b/tests/PHPStan/Rules/Methods/data/bug-9657.php new file mode 100644 index 0000000000..e7a6ac9ba9 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9657.php @@ -0,0 +1,25 @@ += 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/disallow-named-arguments-php-version-scope.php b/tests/PHPStan/Rules/Methods/data/disallow-named-arguments-php-version-scope.php new file mode 100644 index 0000000000..174fef244d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/disallow-named-arguments-php-version-scope.php @@ -0,0 +1,35 @@ += 80000) { + class Foo + { + + public function doFoo(): void + { + $this->doBar(i: 1); + } + + public function doBar(int $i): void + { + + } + + } +} else { + class FooBar + { + + public function doFoo(): void + { + $this->doBar(i: 1); + } + + public function doBar(int $i): void + { + + } + + } +} diff --git a/tests/PHPStan/Rules/Methods/data/dynamic-call.php b/tests/PHPStan/Rules/Methods/data/dynamic-call.php new file mode 100644 index 0000000000..3a917c0c81 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/dynamic-call.php @@ -0,0 +1,62 @@ +$foo(); + echo $this->$string(); + echo $this->$obj(); + echo $this->{self::$name}(); + } + + public function testStaticCall(string $string, object $obj): void + { + $foo = 'bar'; + + echo self::$foo(); + echo self::$string(); + echo self::$obj(); + echo self::{self::$name}(); + } + + public function testScope(): void + { + $param1 = 1; + $param2 = 'str'; + $name1 = 'doFoo'; + if (rand(0, 1)) { + $name = $name1; + $param = $param1; + } else { + $name = 'doQux'; + $param = $param2; + } + + $this->$name($param); // ok + $this->$name1($param); + $this->$name($param1); + $this->$name($param2); + + self::$name($param); // ok + self::$name1($param); + self::$name($param1); + self::$name($param2); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/final-private-method-config-phpversion.php b/tests/PHPStan/Rules/Methods/data/final-private-method-config-phpversion.php new file mode 100644 index 0000000000..6880568c93 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/final-private-method-config-phpversion.php @@ -0,0 +1,11 @@ += 80000) { + class FooBarPhp8orHigher + { + + final private function foo(): void + { + } + } +} + +if (PHP_VERSION_ID < 80000) { + class FooBarPhp7 + { + + final private function foo(): void + { + } + } +} + +if (PHP_VERSION_ID > 70400) { + class FooBarPhp74OrHigher + { + + final private function foo(): void + { + } + } +} + +if (PHP_VERSION_ID < 70400 || PHP_VERSION_ID >= 80100) { + class FooBarBaz + { + + final private function foo(): void + { + } + } +} diff --git a/tests/PHPStan/Rules/Methods/data/final-private-method.php b/tests/PHPStan/Rules/Methods/data/final-private-method.php index 35c5410e30..bb06450219 100644 --- a/tests/PHPStan/Rules/Methods/data/final-private-method.php +++ b/tests/PHPStan/Rules/Methods/data/final-private-method.php @@ -1,4 +1,4 @@ -= 7.4 += 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/illegal-constructor-call-rule-test.php b/tests/PHPStan/Rules/Methods/data/illegal-constructor-call-rule-test.php deleted file mode 100644 index f5198a5089..0000000000 --- a/tests/PHPStan/Rules/Methods/data/illegal-constructor-call-rule-test.php +++ /dev/null @@ -1,103 +0,0 @@ - 1) { - return; - } - $this->__construct($datetime, $timezone); - } - - public function mutate(string $datetime = "now", ?\DateTimeZone $timezone = null): void - { - $this->__construct($datetime, $timezone); - } -} - -class ExtendedDateTimeWithParentCall extends \DateTimeImmutable -{ - public function __construct(string $datetime = "now", ?\DateTimeZone $timezone = null) - { - parent::__construct($datetime, $timezone); - } - - public function mutate(string $datetime = "now", ?\DateTimeZone $timezone = null): void - { - parent::__construct($datetime, $timezone); - } -} - -class ExtendedDateTimeWithSelfCall extends \DateTimeImmutable -{ - public function __construct(string $datetime = "now", ?\DateTimeZone $timezone = null) - { - // Avoid infinite loop - if (count(debug_backtrace()) > 1) { - return; - } - self::__construct($datetime, $timezone); - ExtendedDateTimeWithSelfCall::__construct($datetime, $timezone); - } - - public function mutate(string $datetime = "now", ?\DateTimeZone $timezone = null): void - { - self::__construct($datetime, $timezone); - ExtendedDateTimeWithSelfCall::__construct($datetime, $timezone); - } -} - -class Foo -{ - - public function doFoo() - { - $extendedDateTime = new ExtendedDateTimeWithMethodCall('2022/04/12'); - $extendedDateTime->__construct('2022/04/13'); - } - -} - -abstract class Presenter -{ - - public function __construct() - { - - } - -} - -abstract class BasePresenter extends Presenter -{ - - public function __construct() - { - Presenter::__construct(); - } - -} - -class CatPresenter extends BasePresenter -{ - - public function __construct() - { - Presenter::__construct(); - } - -} - -class DogPresenter extends BasePresenter -{ - - public function __construct() - { - CatPresenter::__construct(); - } - -} 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 @@ +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/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 @@ + + */ + public function doFoo() + { + + } + +} + +/** + * @template T + * @extends Foo + */ +class Bar extends Foo +{ + + /** + * @return static + */ + public function doFoo() + { + + } + +} + +/** + * @template T + * @extends Foo + */ +final class FinalBar extends Foo +{ + + /** + * @return static + */ + public function doFoo() + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php b/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php index d5a333491b..27fa039ef4 100644 --- a/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php +++ b/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php @@ -241,3 +241,35 @@ 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/named-arguments.php b/tests/PHPStan/Rules/Methods/data/named-arguments.php index f5c779f171..25d9ef362b 100644 --- a/tests/PHPStan/Rules/Methods/data/named-arguments.php +++ b/tests/PHPStan/Rules/Methods/data/named-arguments.php @@ -92,6 +92,7 @@ public function doDolor(): void $this->doIpsum(...['a' => 1, 'foo' => 'foo']); $this->doIpsum(...['b' => 1, 'foo' => 'foo']); $this->doIpsum(...[1, 2], 'foo'); + $this->doIpsum(1, 2, foo: 1, bar: 2); } } diff --git a/tests/PHPStan/Rules/Methods/data/no-named-arguments.php b/tests/PHPStan/Rules/Methods/data/no-named-arguments.php new file mode 100644 index 0000000000..e28e91d1d8 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/no-named-arguments.php @@ -0,0 +1,34 @@ += 8.0 + +namespace NoNamedArgumentsMethod; + +class Foo +{ + + /** + * @no-named-arguments + */ + public function doFoo(int $i): void + { + + } + +} + +/** + * @no-named-arguments + */ +class Bar +{ + + public function doFoo(int $i): void + { + + } + +} + +function (Foo $f, Bar $b): void { + $f->doFoo(i: 1); + $b->doFoo(i: 1); +}; diff --git a/tests/PHPStan/Rules/Methods/data/object-shapes.php b/tests/PHPStan/Rules/Methods/data/object-shapes.php index 248eeecc30..8df09d5f93 100644 --- a/tests/PHPStan/Rules/Methods/data/object-shapes.php +++ b/tests/PHPStan/Rules/Methods/data/object-shapes.php @@ -8,10 +8,11 @@ class Foo { - public function doFoo(): void + public function doFoo(Exception $e): void { $this->doBar(new stdClass()); $this->doBar(new Exception()); + $this->doBar($e); } /** diff --git a/tests/PHPStan/Rules/Methods/data/overriden-abstract-trait-method-phpdoc.php b/tests/PHPStan/Rules/Methods/data/overriden-abstract-trait-method-phpdoc.php new file mode 100644 index 0000000000..6a89da7862 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/overriden-abstract-trait-method-phpdoc.php @@ -0,0 +1,46 @@ + + */ +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/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/returnTypes.php b/tests/PHPStan/Rules/Methods/data/returnTypes.php index 1f94e976ab..e7030f8ad8 100644 --- a/tests/PHPStan/Rules/Methods/data/returnTypes.php +++ b/tests/PHPStan/Rules/Methods/data/returnTypes.php @@ -375,7 +375,7 @@ public function misleadingIntReturnType(): \ReturnTypes\integer } } - public function misleadingMixedReturnType(): mixed + /*public function misleadingMixedReturnType(): mixed { if (rand(0, 1)) { return 1; @@ -386,7 +386,7 @@ public function misleadingMixedReturnType(): mixed if (rand(0, 1)) { return new mixed(); } - } + }*/ } class FooChild extends Foo @@ -833,9 +833,8 @@ public function doFoo() /** * @return $this */ - public function doBar() + public function doBar(self $otherInstance) { - $otherInstance = new self(); assert($otherInstance instanceof FooInterface); return $otherInstance; } 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/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/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/Missing/MissingReturnRuleTest.php b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php index ad9fc94be5..b848a761d7 100644 --- a/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php +++ b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php @@ -4,7 +4,8 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -153,12 +154,9 @@ public function testMissingMixedReturnInEmptyBody(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug3488(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->checkExplicitMixedMissingReturn = true; $this->analyse([__DIR__ . '/data/bug-3488.php'], []); } @@ -171,7 +169,7 @@ public function testBug3669(): void $this->analyse([__DIR__ . '/data/bug-3669.php'], []); } - public function dataCheckPhpDocMissingReturn(): array + public static function dataCheckPhpDocMissingReturn(): array { return [ [ @@ -260,9 +258,10 @@ public function dataCheckPhpDocMissingReturn(): array } /** - * @dataProvider dataCheckPhpDocMissingReturn * @param list $errors */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataCheckPhpDocMissingReturn')] public function testCheckPhpDocMissingReturn(bool $checkPhpDocMissingReturn, array $errors): void { $this->checkExplicitMixedMissingReturn = true; @@ -270,7 +269,7 @@ public function testCheckPhpDocMissingReturn(bool $checkPhpDocMissingReturn, arr $this->analyse([__DIR__ . '/data/check-phpdoc-missing-return.php'], $errors); } - public function dataModelMixin(): array + public static function dataModelMixin(): array { return [ [ @@ -282,9 +281,8 @@ public function dataModelMixin(): array ]; } - /** - * @dataProvider dataModelMixin - */ + #[RequiresPhp('>= 8.0')] + #[DataProvider('dataModelMixin')] public function testModelMixin(bool $checkExplicitMixedMissingReturn): void { $this->checkExplicitMixedMissingReturn = $checkExplicitMixedMissingReturn; @@ -297,11 +295,9 @@ public function testModelMixin(bool $checkExplicitMixedMissingReturn): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug6257(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->checkExplicitMixedMissingReturn = true; $this->checkPhpDocMissingReturn = true; $this->analyse([__DIR__ . '/data/bug-6257.php'], [ @@ -325,4 +321,56 @@ public function testBug9309(): void $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 @@ + 5) + throw new Exception(); + + if (rand() == 1) + return 5; + } +} diff --git a/tests/PHPStan/Rules/Missing/data/bug-8463.php b/tests/PHPStan/Rules/Missing/data/bug-8463.php new file mode 100644 index 0000000000..c27592782f --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-8463.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/Namespaces/ExistingNamesInGroupUseRuleTest.php b/tests/PHPStan/Rules/Namespaces/ExistingNamesInGroupUseRuleTest.php index 06a4be03c5..470da7e107 100644 --- a/tests/PHPStan/Rules/Namespaces/ExistingNamesInGroupUseRuleTest.php +++ b/tests/PHPStan/Rules/Namespaces/ExistingNamesInGroupUseRuleTest.php @@ -16,14 +16,17 @@ class ExistingNamesInGroupUseRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ExistingNamesInGroupUseRule( $reflectionProvider, new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), true, + true, ); } diff --git a/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php b/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php index 372a5b311a..e78cd77e65 100644 --- a/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php +++ b/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php @@ -16,14 +16,17 @@ class ExistingNamesInUseRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ExistingNamesInUseRule( $reflectionProvider, new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), true, + true, ); } diff --git a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php index 41c947e937..840bd8b450 100644 --- a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php @@ -7,7 +7,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -23,8 +23,7 @@ protected function getRule(): Rule { return new InvalidBinaryOperationRule( new ExprPrinter(new Printer()), - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false), - true, + new RuleLevelHelper(self::createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), ); } @@ -285,12 +284,9 @@ public function testBug8827(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8827.php'], []); } + #[RequiresPhp('>= 8.0')] public function testRuleWithNullsafeVariant(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/invalid-binary-nullsafe.php'], [ [ 'Binary operation "+" between array|null and \'2\' results in an error.', @@ -304,6 +300,7 @@ public function testBug5309(): void $this->analyse([__DIR__ . '/data/bug-5309.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBinaryMixed(): void { $this->checkExplicitMixed = true; @@ -798,4 +795,16 @@ public function testBenevolentUnion(): void ]); } + public function testBug7863(): void + { + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-7863.php'], [ + [ + 'Binary operation "+" between mixed and array results in an error.', + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php index 3221658bc4..f8163f171b 100644 --- a/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php @@ -5,7 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -16,7 +16,7 @@ class InvalidComparisonOperationRuleTest extends RuleTestCase protected function getRule(): Rule { return new InvalidComparisonOperationRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false), + new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true), ); } @@ -154,12 +154,9 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.0')] public function testRuleWithNullsafeVariant(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/invalid-comparison-nullsafe.php'], [ [ 'Comparison operation "==" between stdClass|null and int results in an error.', @@ -168,4 +165,9 @@ public function testRuleWithNullsafeVariant(): void ]); } + public function testBug11119(): void + { + $this->analyse([__DIR__ . '/data/bug-11119.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php index 5042bb336c..4faae03e3c 100644 --- a/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -19,9 +20,7 @@ class InvalidIncDecOperationRuleTest extends RuleTestCase protected function getRule(): Rule { return new InvalidIncDecOperationRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false), - true, - false, + new RuleLevelHelper(self::createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), ); } @@ -67,6 +66,7 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.0')] public function testMixed(): void { $this->checkExplicitMixed = true; diff --git a/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php index 2475fa3a80..7300106db7 100644 --- a/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -19,8 +20,7 @@ class InvalidUnaryOperationRuleTest extends RuleTestCase protected function getRule(): Rule { return new InvalidUnaryOperationRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false), - true, + new RuleLevelHelper(self::createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), ); } @@ -94,6 +94,7 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.0')] public function testMixed(): void { $this->checkImplicitMixed = true; diff --git a/tests/PHPStan/Rules/Operators/data/bug-11119.php b/tests/PHPStan/Rules/Operators/data/bug-11119.php new file mode 100644 index 0000000000..c0fe6b5feb --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/bug-11119.php @@ -0,0 +1,17 @@ + ($carry instanceof DateTime && $carry < $time) ? $carry : $time, + null + ); +}; diff --git a/tests/PHPStan/Rules/Operators/data/bug-7863.php b/tests/PHPStan/Rules/Operators/data/bug-7863.php new file mode 100644 index 0000000000..3a848d6769 --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/bug-7863.php @@ -0,0 +1,11 @@ +getByType(InitializerExprTypeResolver::class); - return new FunctionAssertRule(new AssertRuleHelper($initializerExprTypeResolver)); + $reflectionProvider = self::createReflectionProvider(); + return new FunctionAssertRule(new AssertRuleHelper( + $initializerExprTypeResolver, + $reflectionProvider, + new UnresolvableTypeHelper(), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new MissingTypehintCheck(true, []), + new GenericObjectTypeCheck(), + true, + true, + )); } public function testRule(): void @@ -54,6 +74,11 @@ public function testRule(): void '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/IncompatibleClassConstantPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php index a66be60a2d..1bd66e2600 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php @@ -5,7 +5,7 @@ use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -32,12 +32,9 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.3')] public function testNativeType(): void { - if (PHP_VERSION_ID < 80300) { - $this->markTestSkipped('Test requires PHP 8.3.'); - } - $this->analyse([__DIR__ . '/data/incompatible-class-constant-phpdoc-native-type.php'], [ [ 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDocNativeType\Foo::BAZ with type string is incompatible with native type int.', @@ -50,4 +47,10 @@ public function testNativeType(): void ]); } + #[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 223fd616b0..dae341a690 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php @@ -10,7 +10,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -20,23 +20,27 @@ class IncompatiblePhpDocTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new IncompatiblePhpDocTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new GenericObjectTypeCheck(), - new UnresolvableTypeHelper(), - new GenericCallableRuleHelper( - new TemplateTypeCheck( - $reflectionProvider, - new ClassNameCheck( - new ClassCaseSensitivityCheck($reflectionProvider, true), - new ClassForbiddenNameCheck(self::getContainer()), + new 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, ), - new GenericObjectTypeCheck(), - $typeAliasResolver, - true, ), ), ); @@ -214,10 +218,6 @@ public function testBug3753(): void 'PHPDoc tag @param for parameter $foo contains unresolvable type.', 20, ], - [ - 'PHPDoc tag @param for parameter $bars contains unresolvable type.', - 28, - ], ]); } @@ -232,12 +232,9 @@ public function testTemplateTypeNativeTypeObject(): void ]); } + #[RequiresPhp('>= 8.1')] public function testEnums(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs PHP 8.1'); - } - $this->analyse([__DIR__ . '/data/generic-enum-param.php'], [ [ 'PHPDoc tag @param for parameter $e contains generic type GenericEnumParam\FooEnum but enum GenericEnumParam\FooEnum is not generic.', @@ -246,12 +243,9 @@ public function testEnums(): void ]); } + #[RequiresPhp('>= 8.1')] public function testValueOfEnum(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs PHP 8.1'); - } - $this->analyse([__DIR__ . '/data/value-of-enum.php'], [ [ 'PHPDoc tag @param for parameter $shouldError with type string is incompatible with native type int.', @@ -264,12 +258,9 @@ public function testValueOfEnum(): void ]); } + #[RequiresPhp('>= 8.0')] public function testConditionalReturnType(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('This test needs PHP 8.0'); - } - $this->analyse([__DIR__ . '/data/incompatible-conditional-return-type.php'], [ [ 'PHPDoc tag @return with type ($p is int ? int : string) is not subtype of native type int.', @@ -289,10 +280,6 @@ public function testParamOut(): void 'Parameter $i for PHPDoc tag @param-out is not passed by reference.', 37, ], - [ - 'PHPDoc tag @param-out for parameter $i contains unresolvable type.', - 44, - ], [ 'PHPDoc tag @param-out for parameter $i contains generic type Exception but class Exception is not generic.', 51, @@ -442,4 +429,52 @@ public function testBug10622B(): void $this->analyse([__DIR__ . '/data/bug-10622b.php'], []); } + public function testParamClosureThis(): void + { + $this->analyse([__DIR__ . '/data/param-closure-this.php'], [ + [ + 'PHPDoc tag @param-closure-this references unknown parameter: $b', + 20, + ], + [ + 'PHPDoc tag @param-closure-this for parameter $i contains unresolvable type.', + 27, + ], + [ + 'PHPDoc tag @param-closure-this for parameter $i contains unresolvable type.', + 34, + ], + [ + 'PHPDoc tag @param-closure-this is for parameter $i with non-Closure type string.', + 41, + ], + [ + 'PHPDoc tag @param-closure-this for parameter $i contains generic type Exception but class Exception is not generic.', + 48, + ], + [ + 'Generic type ParamClosureThisPhpDocRule\FooBar in PHPDoc tag @param-closure-this for parameter $i does not specify all template types of class ParamClosureThisPhpDocRule\FooBar: T, TT', + 55, + ], + [ + 'Type mixed in generic type ParamClosureThisPhpDocRule\FooBar in PHPDoc tag @param-closure-this for parameter $i is not subtype of template type T of int of class ParamClosureThisPhpDocRule\FooBar.', + 55, + ], + [ + 'Generic type ParamClosureThisPhpDocRule\FooBar in PHPDoc tag @param-closure-this for parameter $i does not specify all template types of class ParamClosureThisPhpDocRule\FooBar: T, TT', + 62, + ], + ]); + } + + public function testGenericStatic(): void + { + $this->analyse([__DIR__ . '/data/incompatible-phpdoc-generic-static.php'], [ + [ + 'Generic type static(IncompatiblePhpDocGenericStatic\Foo) in PHPDoc tag @return specifies 2 template types, but class IncompatiblePhpDocGenericStatic\Foo supports only 1: T', + 14, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyHookPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyHookPhpDocTypeRuleTest.php new file mode 100644 index 0000000000..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 d8ec3604b3..c252fa002b 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php @@ -18,7 +18,7 @@ class IncompatiblePropertyPhpDocTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new IncompatiblePropertyPhpDocTypeRule( @@ -30,6 +30,8 @@ protected function getRule(): Rule new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), new GenericObjectTypeCheck(), $typeAliasResolver, diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php index 96ea886257..2ab4973cef 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\PhpDoc; +use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -13,7 +14,7 @@ class IncompatibleSelfOutTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new IncompatibleSelfOutTypeRule(); + return new IncompatibleSelfOutTypeRule(new UnresolvableTypeHelper(), new GenericObjectTypeCheck()); } public function testRule(): void @@ -27,6 +28,34 @@ public function testRule(): void '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 7995c696f4..6b5ba10a37 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use function array_merge; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -14,20 +14,17 @@ class InvalidPHPStanDocTagRuleTest extends RuleTestCase { - private bool $checkAllInvalidPhpDocs; - protected function getRule(): Rule { return new InvalidPHPStanDocTagRule( self::getContainer()->getByType(Lexer::class), self::getContainer()->getByType(PhpDocParser::class), - $this->checkAllInvalidPhpDocs, ); } - public function dataRule(): iterable + public function testRule(): void { - $errors = [ + $this->analyse([__DIR__ . '/data/invalid-phpstan-doc.php'], [ [ 'Unknown PHPDoc tag: @phpstan-extens', 6, @@ -44,30 +41,27 @@ public function dataRule(): iterable 'Unknown PHPDoc tag: @phpstan-varr', 46, ], - ]; - yield [false, $errors]; - yield [true, array_merge($errors, [ [ 'Unknown PHPDoc tag: @phpstan-varr', 56, ], - ])]; + ]); } - /** - * @dataProvider dataRule - * @param list $expectedErrors - */ - public function testRule(bool $checkAllInvalidPhpDocs, array $expectedErrors): void + public function testBug8697(): void { - $this->checkAllInvalidPhpDocs = $checkAllInvalidPhpDocs; - $this->analyse([__DIR__ . '/data/invalid-phpstan-doc.php'], $expectedErrors); + $this->analyse([__DIR__ . '/data/bug-8697.php'], []); } - public function testBug8697(): void + #[RequiresPhp('>= 8.4')] + public function testPropertyHooks(): void { - $this->checkAllInvalidPhpDocs = true; - $this->analyse([__DIR__ . '/data/bug-8697.php'], []); + $this->analyse([__DIR__ . '/data/invalid-phpstan-tag-property-hooks.php'], [ + [ + 'Unknown PHPDoc tag: @phpstan-what', + 9, + ], + ]); } } diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleNoBleedingEdgeTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleNoBleedingEdgeTest.php deleted file mode 100644 index b0bb271349..0000000000 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleNoBleedingEdgeTest.php +++ /dev/null @@ -1,146 +0,0 @@ - - */ -class InvalidPhpDocTagValueRuleNoBleedingEdgeTest extends RuleTestCase -{ - - private bool $checkAllInvalidPhpDocs; - - protected function getRule(): Rule - { - return new InvalidPhpDocTagValueRule( - self::getContainer()->getByType(Lexer::class), - self::getContainer()->getByType(PhpDocParser::class), - $this->checkAllInvalidPhpDocs, - false, - ); - } - - public function dataRule(): iterable - { - $errors = [ - [ - 'PHPDoc tag @param has invalid value (): Unexpected token "\n * ", expected type at offset 13', - 25, - ], - [ - 'PHPDoc tag @param has invalid value (A & B | C $paramNameA): Unexpected token "|", expected variable at offset 72', - 25, - ], - [ - 'PHPDoc tag @param has invalid value ((A & B $paramNameB): Unexpected token "$paramNameB", expected \')\' at offset 105', - 25, - ], - [ - 'PHPDoc tag @param has invalid value (~A & B $paramNameC): Unexpected token "~A", expected type at offset 127', - 25, - ], - [ - 'PHPDoc tag @var has invalid value (): Unexpected token "\n * ", expected type at offset 156', - 25, - ], - [ - 'PHPDoc tag @var has invalid value ($invalid): Unexpected token "$invalid", expected type at offset 165', - 25, - ], - [ - 'PHPDoc tag @var has invalid value ($invalid Foo): Unexpected token "$invalid", expected type at offset 182', - 25, - ], - [ - 'PHPDoc tag @return has invalid value (): Unexpected token "\n * ", expected type at offset 208', - 25, - ], - [ - 'PHPDoc tag @return has invalid value ([int, string]): Unexpected token "[", expected type at offset 220', - 25, - ], - [ - 'PHPDoc tag @return has invalid value (A & B | C): Unexpected token "|", expected TOKEN_OTHER at offset 251', - 25, - ], - [ - 'PHPDoc tag @var has invalid value (\\\Foo|\Bar $test): Unexpected token "\\\\\\\Foo|\\\Bar", expected type at offset 9', - 29, - ], - [ - 'PHPDoc tag @var has invalid value ((Foo|Bar): Unexpected token "*/", expected \')\' at offset 18', - 62, - ], - [ - 'PHPDoc tag @throws has invalid value ((\Exception): Unexpected token "*/", expected \')\' at offset 24', - 72, - ], - [ - 'PHPDoc tag @var has invalid value ((Foo|Bar): Unexpected token "*/", expected \')\' at offset 18', - 81, - ], - [ - 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15', - 89, - ], - [ - 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15', - 92, - ], - ]; - - yield [false, $errors]; - yield [true, array_merge($errors, [ - [ - 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15', - 102, - ], - ])]; - } - - /** - * @dataProvider dataRule - * @param list $expectedErrors - */ - public function testRule(bool $checkAllInvalidPhpDocs, array $expectedErrors): void - { - $this->checkAllInvalidPhpDocs = $checkAllInvalidPhpDocs; - $this->analyse([__DIR__ . '/data/invalid-phpdoc.php'], $expectedErrors); - } - - public function testBug4731(): void - { - $this->checkAllInvalidPhpDocs = true; - $this->analyse([__DIR__ . '/data/bug-4731.php'], []); - } - - public function testBug4731WithoutFirstTag(): void - { - $this->checkAllInvalidPhpDocs = true; - $this->analyse([__DIR__ . '/data/bug-4731-no-first-tag.php'], []); - } - - public function testInvalidTypeInTypeAlias(): void - { - $this->checkAllInvalidPhpDocs = true; - $this->analyse([__DIR__ . '/data/invalid-type-type-alias.php'], [ - [ - 'PHPDoc tag @phpstan-type InvalidFoo has invalid value: Unexpected token "{", expected TOKEN_PHPDOC_EOL at offset 65', - 15, - ], - ]); - } - - public static function getAdditionalConfigFiles(): array - { - // reset bleedingEdge - return []; - } - -} diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php index c726559432..81c0868e3e 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use function array_merge; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -14,21 +14,17 @@ class InvalidPhpDocTagValueRuleTest extends RuleTestCase { - private bool $checkAllInvalidPhpDocs; - protected function getRule(): Rule { return new InvalidPhpDocTagValueRule( self::getContainer()->getByType(Lexer::class), self::getContainer()->getByType(PhpDocParser::class), - $this->checkAllInvalidPhpDocs, - true, ); } - public function dataRule(): iterable + public function testRule(): void { - $errors = [ + $this->analyse([__DIR__ . '/data/invalid-phpdoc.php'], [ [ 'PHPDoc tag @param has invalid value (): Unexpected token "\n * ", expected type at offset 13 on line 2', 6, @@ -97,42 +93,25 @@ public function dataRule(): iterable 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15 on line 1', 91, ], - ]; - - yield [false, $errors]; - yield [true, array_merge($errors, [ [ 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15 on line 1', 101, ], - ])]; - } - - /** - * @dataProvider dataRule - * @param list $expectedErrors - */ - public function testRule(bool $checkAllInvalidPhpDocs, array $expectedErrors): void - { - $this->checkAllInvalidPhpDocs = $checkAllInvalidPhpDocs; - $this->analyse([__DIR__ . '/data/invalid-phpdoc.php'], $expectedErrors); + ]); } public function testBug4731(): void { - $this->checkAllInvalidPhpDocs = true; $this->analyse([__DIR__ . '/data/bug-4731.php'], []); } public function testBug4731WithoutFirstTag(): void { - $this->checkAllInvalidPhpDocs = true; $this->analyse([__DIR__ . '/data/bug-4731-no-first-tag.php'], []); } public function testInvalidTypeInTypeAlias(): void { - $this->checkAllInvalidPhpDocs = true; $this->analyse([__DIR__ . '/data/invalid-type-type-alias.php'], [ [ 'PHPDoc tag @phpstan-type InvalidFoo has invalid value: Unexpected token "{", expected TOKEN_PHPDOC_EOL at offset 65 on line 3', @@ -143,8 +122,38 @@ public function testInvalidTypeInTypeAlias(): void public function testIgnoreWithinPhpDoc(): void { - $this->checkAllInvalidPhpDocs = true; $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 9d4dc0bb3e..1c3f33d575 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php @@ -10,7 +10,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -20,19 +20,22 @@ class InvalidPhpDocVarTagTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new InvalidPhpDocVarTagTypeRule( self::getContainer()->getByType(FileTypeMapper::class), $reflectionProvider, new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), new GenericObjectTypeCheck(), - new MissingTypehintCheck(true, true, true, true, []), + new MissingTypehintCheck(true, []), new UnresolvableTypeHelper(), true, true, + true, ); } @@ -107,8 +110,12 @@ public function testRule(): void 73, ], [ - 'PHPDoc tag @var for variable $foo contains unknown class InvalidVarTagType\Blabla.', + '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', ], ]); @@ -151,11 +158,9 @@ public function testBug4486Namespace(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug6252(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->analyse([__DIR__ . '/data/bug-6252.php'], []); } diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php index 2328aeb0d7..60cf6874c7 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php @@ -9,6 +9,8 @@ use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -99,7 +101,7 @@ public function testThrowsWithRequireExtends(): void ]); } - public function dataMergeInheritedPhpDocs(): array + public static function dataMergeInheritedPhpDocs(): array { return [ [ @@ -120,16 +122,14 @@ public function dataMergeInheritedPhpDocs(): array ]; } - /** - * @dataProvider dataMergeInheritedPhpDocs - */ + #[DataProvider('dataMergeInheritedPhpDocs')] public function testMergeInheritedPhpDocs( string $className, string $method, string $expectedType, ): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $reflection = $reflectionProvider->getClass($className); $method = $reflection->getNativeMethod($method); $throwsType = $method->getThrowType(); @@ -137,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 index 932a09c299..4341b7754e 100644 --- a/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php @@ -3,6 +3,11 @@ namespace PHPStan\Rules\PhpDoc; use PHPStan\Reflection\InitializerExprTypeResolver; +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; @@ -15,7 +20,22 @@ class MethodAssertRuleTest extends RuleTestCase protected function getRule(): Rule { $initializerExprTypeResolver = self::getContainer()->getByType(InitializerExprTypeResolver::class); - return new MethodAssertRule(new AssertRuleHelper($initializerExprTypeResolver)); + $reflectionProvider = self::createReflectionProvider(); + return new MethodAssertRule(new AssertRuleHelper( + $initializerExprTypeResolver, + $reflectionProvider, + new UnresolvableTypeHelper(), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new MissingTypehintCheck(true, []), + new GenericObjectTypeCheck(), + true, + true, + )); } public function testRule(): void @@ -54,6 +74,67 @@ public function testRule(): void '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, + ], ]); } diff --git a/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php index 48258202dc..35ab5c2e4e 100644 --- a/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -95,4 +96,10 @@ 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 index 5a24e75502..8a9546216e 100644 --- a/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionClassRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionClassRuleTest.php @@ -7,7 +7,7 @@ use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,41 +17,38 @@ class RequireExtendsDefinitionClassRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $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 { - $enumError = 'PHPDoc tag @phpstan-require-extends cannot contain non-class type IncompatibleRequireExtends\SomeEnum.'; - $enumTip = null; - if (PHP_VERSION_ID < 80100) { - $enumError = 'PHPDoc tag @phpstan-require-extends contains unknown class IncompatibleRequireExtends\SomeEnum.'; - $enumTip = 'Learn more at https://phpstan.org/user-guide/discovering-symbols'; - } - $this->analyse([__DIR__ . '/data/incompatible-require-extends.php'], [ [ 'PHPDoc tag @phpstan-require-extends cannot contain non-class type IncompatibleRequireExtends\SomeTrait.', 8, ], [ - 'PHPDoc tag @phpstan-require-extends cannot contain non-class type IncompatibleRequireExtends\SomeInterface.', + '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.', ], [ - $enumError, + 'PHPDoc tag @phpstan-require-extends cannot contain non-class type IncompatibleRequireExtends\SomeEnum.', 18, - $enumTip, ], [ 'PHPDoc tag @phpstan-require-extends contains unknown class IncompatibleRequireExtends\TypeDoesNotExist.', @@ -75,13 +72,24 @@ public function testRule(): void 121, ], [ - 'PHPDoc tag @phpstan-require-extends contains non-object type IncompatibleRequireExtends\UnresolvableExtendsInterface&stdClass.', + '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 index 746c3b9007..e345685e67 100644 --- a/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionTraitRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionTraitRuleTest.php @@ -7,6 +7,7 @@ use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -16,7 +17,7 @@ class RequireExtendsDefinitionTraitRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new RequireExtendsDefinitionTraitRule( $reflectionProvider, @@ -24,12 +25,16 @@ protected function getRule(): Rule 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'], [ @@ -45,6 +50,16 @@ public function testRule(): void '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 index 06cb9fdf88..807bbbc7d6 100644 --- a/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionClassRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionClassRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -16,9 +17,10 @@ protected function getRule(): Rule return new RequireImplementsDefinitionClassRule(); } + #[RequiresPhp('>= 8.1')] public function testRule(): void { - $expectedErrors = [ + $this->analyse([__DIR__ . '/data/incompatible-require-implements.php'], [ [ 'PHPDoc tag @phpstan-require-implements is only valid on trait.', 40, @@ -27,9 +29,7 @@ public function testRule(): void 'PHPDoc tag @phpstan-require-implements is only valid on trait.', 45, ], - ]; - - $this->analyse([__DIR__ . '/data/incompatible-require-implements.php'], $expectedErrors); + ]); } } diff --git a/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionTraitRuleTest.php b/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionTraitRuleTest.php index ad2f2f7cba..1d0421388f 100644 --- a/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionTraitRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionTraitRuleTest.php @@ -7,7 +7,7 @@ use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,36 +17,32 @@ class RequireImplementsDefinitionTraitRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $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 { - $enumError = 'PHPDoc tag @phpstan-require-implements cannot contain non-interface type IncompatibleRequireImplements\SomeEnum.'; - $enumTip = null; - if (PHP_VERSION_ID < 80100) { - $enumError = 'PHPDoc tag @phpstan-require-implements contains unknown class IncompatibleRequireImplements\SomeEnum.'; - $enumTip = 'Learn more at https://phpstan.org/user-guide/discovering-symbols'; - } - $expectedErrors = [ [ 'PHPDoc tag @phpstan-require-implements cannot contain non-interface type IncompatibleRequireImplements\SomeTrait.', 8, ], [ - $enumError, + 'PHPDoc tag @phpstan-require-implements cannot contain non-interface type IncompatibleRequireImplements\SomeEnum.', 13, - $enumTip, ], [ 'PHPDoc tag @phpstan-require-implements contains unknown class IncompatibleRequireImplements\TypeDoesNotExist.', 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 index f3ea56d12f..d515b2c8af 100644 --- a/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php @@ -2,8 +2,10 @@ namespace PHPStan\Rules\PhpDoc; +use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPStan\Type\FileTypeMapper; /** * @extends RuleTestCase @@ -13,7 +15,13 @@ class VarTagChangedExpressionTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new VarTagChangedExpressionTypeRule(new VarTagTypeRuleHelper(true, true)); + return new VarTagChangedExpressionTypeRule(new VarTagTypeRuleHelper( + self::getContainer()->getByType(TypeNodeResolver::class), + self::getContainer()->getByType(FileTypeMapper::class), + self::createReflectionProvider(), + true, + true, + )); } public function testRule(): void @@ -52,22 +60,44 @@ public function testBug10130(): void { $this->analyse([__DIR__ . '/data/bug-10130.php'], [ [ - 'PHPDoc tag @var with type array is not subtype of type array.', + '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.', + '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}.', + '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.', + '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 6083de943d..513c9118a7 100644 --- a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php @@ -2,10 +2,12 @@ namespace PHPStan\Rules\PhpDoc; +use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -13,8 +15,6 @@ class WrongVariableNameInVarTagRuleTest extends RuleTestCase { - private bool $checkTypeAgainstNativeType = false; - private bool $checkTypeAgainstPhpDocType = false; private bool $strictWideningCheck = false; @@ -23,14 +23,27 @@ protected function getRule(): Rule { return new WrongVariableNameInVarTagRule( self::getContainer()->getByType(FileTypeMapper::class), - new VarTagTypeRuleHelper($this->checkTypeAgainstPhpDocType, $this->strictWideningCheck), - $this->checkTypeAgainstNativeType, + 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, @@ -71,6 +84,10 @@ 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.', 126, @@ -182,12 +199,46 @@ public function testBug4505(): void $this->analyse([__DIR__ . '/data/bug-4505.php'], []); } - public function testEnums(): void + 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 { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs PHP 8.1'); - } + $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.', @@ -196,7 +247,7 @@ public function testEnums(): void ]); } - public function dataReportWrongType(): iterable + public static function dataReportWrongType(): iterable { $nativeCheckOnly = [ [ @@ -253,12 +304,9 @@ public function dataReportWrongType(): iterable ], ]; - yield [false, false, false, []]; - yield [true, false, false, $nativeCheckOnly]; - yield [true, false, true, $nativeCheckOnly]; - yield [false, true, false, []]; - yield [false, true, true, []]; - yield [true, true, false, [ + 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, @@ -349,7 +397,7 @@ public function dataReportWrongType(): iterable 204, ], ]]; - yield [true, true, true, [ + yield [true, true, [ [ 'PHPDoc tag @var with type string|null is not subtype of native type string.', 14, @@ -460,55 +508,66 @@ public function dataReportWrongType(): iterable 204, ], [ - 'PHPDoc tag @var with type array|null is not subtype of type array{id: int}|null.', + 'PHPDoc tag @var with type array|null is not subtype of type array{id: int}|null.', 235, ], ]]; } - /** - * @dataProvider dataPermutateCheckTypeAgainst - */ - public function testEmptyArrayInitWithWiderPhpDoc(bool $checkTypeAgainstNativeType, bool $checkTypeAgainstPhpDocType): void + #[DataProvider('dataPermutateCheckTypeAgainst')] + public function testEmptyArrayInitWithWiderPhpDoc(bool $checkTypeAgainstPhpDocType): void { - $this->checkTypeAgainstNativeType = $checkTypeAgainstNativeType; $this->checkTypeAgainstPhpDocType = $checkTypeAgainstPhpDocType; - - $errors = !$checkTypeAgainstNativeType - ? [] - : [ - [ - 'PHPDoc tag @var with type int is not subtype of native type array{}.', - 24, - ], - ]; - - $this->analyse([__DIR__ . '/data/var-above-empty-array-widening.php'], $errors); + $this->analyse([__DIR__ . '/data/var-above-empty-array-widening.php'], [ + [ + 'PHPDoc tag @var with type int is not subtype of native type array{}.', + 24, + ], + ]); } - public function dataPermutateCheckTypeAgainst(): iterable + public static function dataPermutateCheckTypeAgainst(): iterable { - yield [true, true]; - yield [false, true]; - yield [true, false]; - yield [false, false]; + yield [true]; + yield [false]; } /** - * @dataProvider dataReportWrongType * @param list $expectedErrors */ + #[DataProvider('dataReportWrongType')] public function testReportWrongType( - bool $checkTypeAgainstNativeType, bool $checkTypeAgainstPhpDocType, bool $strictWideningCheck, array $expectedErrors, ): void { - $this->checkTypeAgainstNativeType = $checkTypeAgainstNativeType; $this->checkTypeAgainstPhpDocType = $checkTypeAgainstPhpDocType; $this->strictWideningCheck = $strictWideningCheck; $this->analyse([__DIR__ . '/data/wrong-var-native-type.php'], $expectedErrors); } + 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-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-4227.php b/tests/PHPStan/Rules/PhpDoc/data/bug-4227.php index 440577cae6..41bc50eae0 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/bug-4227.php +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-4227.php @@ -1,4 +1,4 @@ -= 7.4 + [], 'branches' => ['names' => [], 'exclude' => false]]; + } + else { + return 0; + } + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-6692.php b/tests/PHPStan/Rules/PhpDoc/data/bug-6692.php new file mode 100644 index 0000000000..41bd197ed9 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-6692.php @@ -0,0 +1,27 @@ += 8.0 + +namespace Bug6692; + +/** + * @template T + */ +class Wrapper +{ + /** + * @return $this + */ + public function change(): static + { + return $this; + } +} + +/** + * @template T + * @extends Wrapper + * + * @method self change() + */ +class SubWrapper extends Wrapper +{ +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-7240.php b/tests/PHPStan/Rules/PhpDoc/data/bug-7240.php index da13e6f11b..f5a764e1c9 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/bug-7240.php +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-7240.php @@ -1,4 +1,4 @@ -= 7.4 + $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/incompatible-param-immediately-invoked-callable.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-param-immediately-invoked-callable.php new file mode 100644 index 0000000000..79cd19f116 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-param-immediately-invoked-callable.php @@ -0,0 +1,80 @@ + + */ + public function doFoo() + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-hook-phpdoc-types.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-hook-phpdoc-types.php new file mode 100644 index 0000000000..b1ce3b8762 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-hook-phpdoc-types.php @@ -0,0 +1,66 @@ += 8.4 + +namespace IncompatiblePropertyHookPhpDocTypes; + +class Foo +{ + + public int $i { + /** @return string */ + get { + return $this->i; + } + } + + public int $j { + /** @return string */ + set { + $this->j = 1; + } + } + + public int $k { + /** + * @param string $value + * @param-out int $value + */ + set { + $this->k = 1; + } + } + + public int $l { + /** @param \stdClass&\Exception $value */ + set { + + } + } + + public \Exception $m { + /** @param \Exception $value */ + set { + + } + } + +} + +/** @template T */ +class GenericFoo +{ + + public int $n { + /** @param int|callable(T): T $value */ + set (int|callable $value) { + + } + } + + public int $o { + /** @param int|callable<\stdClass>(T): T $value */ + set (int|callable $value) { + + } + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-native-types.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-native-types.php index 56c5f7bc60..5c6746437b 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-native-types.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-native-types.php @@ -1,4 +1,4 @@ -= 7.4 += 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 index a0c4e977a0..c60ff3ce6c 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php @@ -27,3 +27,79 @@ public function three(); */ 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/invalid-phpdoc-definitions.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php index f52cae0d04..74ad37a779 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php @@ -23,3 +23,20 @@ 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-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-var-tag-type.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-var-tag-type.php index fd9688445d..cd996b4a21 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-var-tag-type.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-var-tag-type.php @@ -71,6 +71,12 @@ public function 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 index fa603eb85f..ad07ee066d 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/method-assert.php +++ b/tests/PHPStan/Rules/PhpDoc/data/method-assert.php @@ -80,3 +80,136 @@ public function negate2(int $i): void { } } + +class AbsentTypeChecks +{ + + /** @var string|null */ + public $fooProp; + + /** + * @phpstan-assert-if-true string&int $a + * @phpstan-assert string&int $this->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/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/template-type-native-type-object.php b/tests/PHPStan/Rules/PhpDoc/data/template-type-native-type-object.php index 823fb21600..215b330ff9 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/template-type-native-type-object.php +++ b/tests/PHPStan/Rules/PhpDoc/data/template-type-native-type-object.php @@ -1,4 +1,4 @@ -= 7.4 + */ + $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 index a75b82f714..789c48ccfb 100644 --- a/tests/PHPStan/Rules/Playground/FunctionNeverRuleTest.php +++ b/tests/PHPStan/Rules/Playground/FunctionNeverRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -16,6 +17,7 @@ protected function getRule(): Rule return new FunctionNeverRule(new NeverRuleHelper()); } + #[RequiresPhp('>= 8.1')] public function testRule(): void { $this->analyse([__DIR__ . '/data/function-never.php'], [ diff --git a/tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php b/tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php index 583c6a5a5f..35b3d20463 100644 --- a/tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php +++ b/tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -16,6 +17,7 @@ protected function getRule(): Rule return new MethodNeverRule(new NeverRuleHelper()); } + #[RequiresPhp('>= 8.1')] public function testRule(): void { $this->analyse([__DIR__ . '/data/method-never.php'], [ diff --git a/tests/PHPStan/Rules/Playground/PhpdocCommentRuleTest.php b/tests/PHPStan/Rules/Playground/PhpdocCommentRuleTest.php new file mode 100644 index 0000000000..7642b2356f --- /dev/null +++ b/tests/PHPStan/Rules/Playground/PhpdocCommentRuleTest.php @@ -0,0 +1,33 @@ + + */ +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/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 @@ +createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new AccessPropertiesInAssignRule( - new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), true, true), + new AccessPropertiesCheck($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new PhpVersion(PHP_VERSION_ID), true, true, true), ); } @@ -40,10 +42,6 @@ public function testRule(): void public function testRuleAssignOp(): void { - if (PHP_VERSION_ID < 70400) { - self::markTestSkipped('Test requires PHP 7.4.'); - } - $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/access-properties-assign-op.php'], [ [ @@ -98,6 +96,21 @@ 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'; @@ -125,4 +138,43 @@ 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, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index 7697f826e3..62cb41086f 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -2,9 +2,12 @@ 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; @@ -22,8 +25,8 @@ class AccessPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); - return new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, $this->checkUnionTypes, false, false, true, false), true, $this->checkDynamicProperties); + $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 @@ -169,6 +172,11 @@ public function testAccessProperties(): void 'Cannot access property $selfOrNull on TestAccessProperties\RevertNonNullabilityForIsset|null.', 407, ], + [ + 'Access to an undefined property object::$baz.', + 438, + $tipText, + ], ], ); } @@ -301,9 +309,6 @@ public function testAccessPropertiesWithoutUnionTypes(): void public function testRuleAssignOp(): void { - if (PHP_VERSION_ID < 70400) { - self::markTestSkipped('Test requires PHP 7.4.'); - } $this->checkThisOnly = false; $this->checkUnionTypes = true; $this->checkDynamicProperties = false; @@ -341,6 +346,17 @@ public function testAccessPropertiesOnThisOnly(): void ); } + 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; @@ -522,11 +538,9 @@ public function testBug4808(): void $this->analyse([__DIR__ . '/data/bug-4808.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug5868(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } $this->checkThisOnly = false; $this->checkUnionTypes = true; $this->checkDynamicProperties = false; @@ -550,12 +564,9 @@ public function testBug5868(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug6385(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->checkThisOnly = false; $this->checkUnionTypes = true; $this->checkDynamicProperties = false; @@ -575,11 +586,9 @@ public function testBug6385(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug6566(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->checkThisOnly = false; $this->checkUnionTypes = true; $this->checkDynamicProperties = false; @@ -625,13 +634,23 @@ public function testBug3659(): void $this->analyse([__DIR__ . '/data/bug-3659.php'], $errors); } - public function dataDynamicProperties(): array + 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\Baz::$dynamicProperty.', - 23, + '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, ], ]; @@ -652,37 +671,51 @@ public function dataDynamicProperties(): array 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.', - 14, + 20, $tipText, ], [ 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', - 15, + 21, $tipText, ], [ 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', - 16, + 22, $tipText, ], - ], $errors); + [ + 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', + 29, + $tipText, + ], + ]); $errorsWithMore = array_merge($errorsWithMore, [ [ 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', - 26, + 32, $tipText, ], [ 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', - 27, + 33, $tipText, ], [ 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', - 28, + 34, $tipText, ], ]); @@ -690,32 +723,32 @@ public function dataDynamicProperties(): array $otherErrors = [ [ 'Access to an undefined property DynamicProperties\FinalFoo::$dynamicProperty.', - 36, + 42, $tipText, ], [ 'Access to an undefined property DynamicProperties\FinalFoo::$dynamicProperty.', - 37, + 43, $tipText, ], [ 'Access to an undefined property DynamicProperties\FinalFoo::$dynamicProperty.', - 38, + 44, $tipText, ], [ 'Access to an undefined property DynamicProperties\FinalBar::$dynamicProperty.', - 41, + 47, $tipText, ], [ 'Access to an undefined property DynamicProperties\FinalBar::$dynamicProperty.', - 42, + 48, $tipText, ], [ 'Access to an undefined property DynamicProperties\FinalBar::$dynamicProperty.', - 43, + 49, $tipText, ], ]; @@ -724,7 +757,7 @@ public function dataDynamicProperties(): array [false, PHP_VERSION_ID < 80200 ? [ [ 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', - 23, + 29, $tipText, ], ] : array_merge($errors, $otherErrors)], @@ -733,9 +766,9 @@ public function dataDynamicProperties(): array } /** - * @dataProvider dataDynamicProperties * @param list $errors */ + #[DataProvider('dataDynamicProperties')] public function testDynamicProperties(bool $checkDynamicProperties, array $errors): void { $this->checkThisOnly = false; @@ -769,7 +802,7 @@ public function testBug3171OnDynamicProperties(): void $this->analyse([__DIR__ . '/data/bug-3171.php'], []); } - public function dataTrueAndFalse(): array + public static function dataTrueAndFalse(): array { return [ [true], @@ -777,9 +810,7 @@ public function dataTrueAndFalse(): array ]; } - /** - * @dataProvider dataTrueAndFalse - */ + #[DataProvider('dataTrueAndFalse')] public function testPhp82AndDynamicProperties(bool $b): void { $errors = []; @@ -790,16 +821,21 @@ public function testPhp82AndDynamicProperties(bool $b): void 34, $tipText, ]; + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', + 71, + $tipText, + ]; if ($b) { $errors[] = [ 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', - 71, + 78, $tipText, ]; } $errors[] = [ 'Access to an undefined property Php82DynamicProperties\FinalHelloWorld::$world.', - 105, + 112, $tipText, ]; } elseif ($b) { @@ -808,9 +844,14 @@ public function testPhp82AndDynamicProperties(bool $b): void 71, $tipText, ]; + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', + 78, + $tipText, + ]; $errors[] = [ 'Access to an undefined property Php82DynamicProperties\FinalHelloWorld::$world.', - 105, + 112, $tipText, ]; } @@ -820,9 +861,7 @@ public function testPhp82AndDynamicProperties(bool $b): void $this->analyse([__DIR__ . '/data/php-82-dynamic-properties.php'], $errors); } - /** - * @dataProvider dataTrueAndFalse - */ + #[DataProvider('dataTrueAndFalse')] public function testPhp82AndDynamicPropertiesAllow(bool $b): void { $errors = []; @@ -901,12 +940,9 @@ public function testConflictingAnnotationProperty(): void $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug8536(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->checkThisOnly = false; $this->checkUnionTypes = true; $this->checkDynamicProperties = true; @@ -937,16 +973,143 @@ public function testBug8629(): void $this->analyse([__DIR__ . '/data/bug-8629.php'], []); } - public function testBug9694(): void + public function testDynamicStringableAccess(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } + $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'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php index e8cd8306de..01b3133fa6 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php @@ -8,7 +8,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -18,15 +17,18 @@ 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, false, true, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), + true, ), ); } @@ -47,9 +49,6 @@ public function testRule(): void public function testRuleAssignOp(): void { - if (PHP_VERSION_ID < 70400) { - self::markTestSkipped('Test requires PHP 7.4.'); - } $this->analyse([__DIR__ . '/data/access-static-properties-assign-op.php'], [ [ 'Access to an undefined static property AccessStaticProperties\AssignOpNonexistentProperty::$flags.', diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index 14e4779f22..deba1aeb7c 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -8,7 +8,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -18,22 +17,22 @@ class AccessStaticPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new AccessStaticPropertiesRule( $reflectionProvider, - new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), + 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 (PHP_VERSION_ID >= 80200) { - $this->markTestSkipped('Test is not for PHP 8.2.'); - } $this->analyse([__DIR__ . '/data/access-static-properties.php'], [ [ 'Access to an undefined static property FooAccessStaticProperties::$bar.', @@ -51,162 +50,9 @@ public function testAccessStaticProperties(): void 'Static access to instance property FooAccessStaticProperties::$loremIpsum.', 26, ], - [ - 'IpsumAccessStaticProperties::ipsum() accesses parent::$lorem but IpsumAccessStaticProperties does not extend any class.', - 42, - ], - [ - 'Access to protected property $foo of class FooAccessStaticProperties.', - 44, - ], - [ - 'Access to static property $test on an unknown class UnknownStaticProperties.', - 47, - 'Learn more at https://phpstan.org/user-guide/discovering-symbols', - ], - [ - 'Access to an undefined static property static(IpsumAccessStaticProperties)::$baz.', - 53, - ], - [ - 'Access to an undefined static property static(IpsumAccessStaticProperties)::$nonexistent.', - 55, - ], - [ - 'Access to an undefined static property static(IpsumAccessStaticProperties)::$emptyBaz.', - 63, - ], - [ - 'Access to an undefined static property static(IpsumAccessStaticProperties)::$emptyNonexistent.', - 65, - ], - [ - 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherNonexistent.', - 71, - ], - [ - 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherNonexistent.', - 72, - ], - [ - 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherEmptyNonexistent.', - 75, - ], - [ - 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherEmptyNonexistent.', - 78, - ], - [ - 'Accessing self::$staticFooProperty outside of class scope.', - 84, - ], - [ - 'Accessing static::$staticFooProperty outside of class scope.', - 85, - ], - [ - 'Accessing parent::$staticFooProperty outside of class scope.', - 86, - ], - [ - 'Access to protected property $foo of class FooAccessStaticProperties.', - 89, - ], [ 'Static access to instance property FooAccessStaticProperties::$loremIpsum.', - 90, - ], - [ - 'Access to an undefined static property FooAccessStaticProperties::$nonexistent.', - 94, - ], - [ - 'Access to static property $test on an unknown class NonexistentClass.', - 97, - 'Learn more at https://phpstan.org/user-guide/discovering-symbols', - ], - [ - 'Access to an undefined static property FooAccessStaticProperties&SomeInterface::$nonexistent.', - 108, - ], - [ - 'Cannot access static property $foo on int|string.', - 113, - ], - [ - 'Class FooAccessStaticProperties referenced with incorrect case: FOOAccessStaticPropertieS.', - 119, - ], - [ - 'Access to an undefined static property FooAccessStaticProperties::$unknownProperties.', - 119, - ], - [ - 'Class FooAccessStaticProperties referenced with incorrect case: FOOAccessStaticPropertieS.', - 120, - ], - [ - 'Static access to instance property FooAccessStaticProperties::$loremIpsum.', - 120, - ], - [ - 'Class FooAccessStaticProperties referenced with incorrect case: FOOAccessStaticPropertieS.', - 121, - ], - [ - 'Access to protected property $foo of class FooAccessStaticProperties.', - 121, - ], - [ - 'Class FooAccessStaticProperties referenced with incorrect case: FOOAccessStaticPropertieS.', - 122, - ], - [ - 'Access to an undefined static property ClassOrString|string::$unknownProperty.', - 141, - ], - [ - 'Static access to instance property ClassOrString::$instanceProperty.', - 152, - ], - [ - 'Access to static property $foo on an unknown class TraitWithStaticProperty.', - 209, - 'Learn more at https://phpstan.org/user-guide/discovering-symbols', - ], - [ - 'Static access to instance property AccessWithStatic::$bar.', - 223, - ], - [ - 'Access to an undefined static property static(AccessWithStatic)::$nonexistent.', - 224, - ], - ]); - } - - public function testAccessStaticPropertiesPhp82(): void - { - if (PHP_VERSION_ID < 80200) { - $this->markTestSkipped('Test requires PHP 8.2.'); - } - - $this->analyse([__DIR__ . '/data/access-static-properties.php'], [ - [ - 'Access to an undefined static property FooAccessStaticProperties::$bar.', - 23, - ], - [ - 'Access to an undefined static property BarAccessStaticProperties::$bar.', - 24, - ], - [ - 'Access to an undefined static property FooAccessStaticProperties::$bar.', - 25, - ], - [ - 'Static access to instance property FooAccessStaticProperties::$loremIpsum.', - 26, + 32, ], [ 'IpsumAccessStaticProperties::ipsum() accesses parent::$lorem but IpsumAccessStaticProperties does not extend any class.', @@ -411,14 +257,19 @@ public function testAccessStaticPropertiesPhp82(): void 'Access to an undefined static property AllowsDynamicProperties::$foo.', 248, ], + [ + 'Static access to instance property ParentClassWithInstanceProperty::$i.', + 267, + ], + [ + 'Access to an undefined static property ParentClassWithInstanceProperty::$j.', + 268, + ], ]); } public function testRuleAssignOp(): void { - if (PHP_VERSION_ID < 70400) { - self::markTestSkipped('Test requires PHP 7.4.'); - } $this->analyse([__DIR__ . '/data/access-static-properties-assign-op.php'], [ [ 'Access to an undefined static property AccessStaticProperties\AssignOpNonexistentProperty::$flags.', @@ -439,14 +290,12 @@ public function testBug5143(): void public function testBug6809(): void { - $errors = []; - if (PHP_VERSION_ID >= 80200) { - $errors[] = [ + $this->analyse([__DIR__ . '/data/bug-6809.php'], [ + [ 'Access to an undefined static property static(Bug6809\HelloWorld)::$coolClass.', 7, - ]; - } - $this->analyse([__DIR__ . '/data/bug-6809.php'], $errors); + ], + ]); } public function testBug8333(): void diff --git a/tests/PHPStan/Rules/Properties/Bug7074Test.php b/tests/PHPStan/Rules/Properties/Bug7074Test.php index 3748675490..8e1ef0c3e0 100644 --- a/tests/PHPStan/Rules/Properties/Bug7074Test.php +++ b/tests/PHPStan/Rules/Properties/Bug7074Test.php @@ -15,7 +15,7 @@ class Bug7074Test extends RuleTestCase protected function getRule(): Rule { - return new DefaultValueTypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false)); + return new DefaultValueTypesAssignedToPropertiesRule(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true)); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php index 5c92162410..1bc6512618 100644 --- a/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php @@ -5,7 +5,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -15,7 +14,7 @@ class DefaultValueTypesAssignedToPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new DefaultValueTypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false)); + return new DefaultValueTypesAssignedToPropertiesRule(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true)); } public function testDefaultValueTypesAssignedToProperties(): void @@ -58,9 +57,6 @@ public function testBug5607(): void public function testBug7933(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->analyse([__DIR__ . '/data/bug-7933.php'], []); } diff --git a/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php index 97a9f1b1f6..ac30206c91 100644 --- a/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php @@ -9,6 +9,7 @@ use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; use const PHP_VERSION_ID; /** @@ -21,17 +22,20 @@ class ExistingClassesInPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return new ExistingClassesInPropertiesRule( $reflectionProvider, new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), ), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersion), true, false, + true, ); } @@ -143,7 +147,7 @@ public function testPromotedProperties(): void ]); } - public function dataIntersectionTypes(): array + public static function dataIntersectionTypes(): array { return [ [80000, []], @@ -164,9 +168,9 @@ public function dataIntersectionTypes(): array } /** - * @dataProvider dataIntersectionTypes * @param list $errors */ + #[DataProvider('dataIntersectionTypes')] public function testIntersectionTypes(int $phpVersion, array $errors): void { $this->phpVersion = $phpVersion; 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/MissingPropertyTypehintRuleTest.php b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php index bd92fe752e..e17100bc38 100644 --- a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php @@ -14,7 +14,7 @@ class MissingPropertyTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - return new MissingPropertyTypehintRule(new MissingTypehintCheck(true, true, true, true, [])); + return new MissingPropertyTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -54,6 +54,10 @@ public function testRule(): void 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, + ], ]); } diff --git a/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php index c7a90b03ab..b503c69e45 100644 --- a/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -6,8 +6,8 @@ use PHPStan\Reflection\PropertyReflection; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use function in_array; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -59,10 +59,6 @@ private function isEntityId(PropertyReflection $property, string $propertyName): public function testRule(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/missing-readonly-property-assign-phpdoc.php'], [ [ 'Class MissingReadOnlyPropertyAssignPhpDoc\Foo has an uninitialized @readonly property $unassigned. Assign it in the constructor.', @@ -147,12 +143,9 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1')] public function testRuleIgnoresNativeReadonly(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/missing-readonly-property-assign-phpdoc-and-native.php'], []); } diff --git a/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php index 02e81f3f55..6d1772cc75 100644 --- a/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php @@ -6,9 +6,9 @@ use PHPStan\Reflection\PropertyReflection; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use function in_array; use function strpos; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -16,6 +16,8 @@ class MissingReadOnlyPropertyAssignRuleTest extends RuleTestCase { + private bool $shouldNarrowMethodScopeFromConstructor = false; + protected function getRule(): Rule { return new MissingReadOnlyPropertyAssignRule( @@ -31,6 +33,11 @@ protected function getRule(): Rule ); } + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return $this->shouldNarrowMethodScopeFromConstructor; + } + protected function getReadWritePropertiesExtensions(): array { return [ @@ -80,12 +87,9 @@ public function isInitialized(PropertyReflection $property, string $propertyName ]; } + #[RequiresPhp('>= 8.1')] public function testRule(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/missing-readonly-property-assign.php'], [ [ 'Class MissingReadOnlyPropertyAssign\Foo has an uninitialized readonly property $unassigned. Assign it in the constructor.', @@ -154,57 +158,39 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug7119(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-7119.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug7314(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-7314.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug8412(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-8412.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug8958(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-8958.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug8563(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-8563.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug6402(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-6402.php'], [ [ 'Access to an uninitialized readonly property Bug6402\SomeModel2::$views.', @@ -213,21 +199,15 @@ public function testBug6402(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug7198(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-7198.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug7649(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-7649.php'], [ [ 'Class Bug7649\Foo has an uninitialized readonly property $bar. Assign it in the constructor.', @@ -236,12 +216,9 @@ public function testBug7649(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug9577(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/../Classes/data/bug-9577.php'], [ [ 'Class Bug9577\SpecializedException2 has an uninitialized readonly property $message. Assign it in the constructor.', @@ -250,12 +227,9 @@ public function testBug9577(): void ]); } + #[RequiresPhp('>= 8.3')] public function testAnonymousReadonlyClass(): void { - if (PHP_VERSION_ID < 80300) { - $this->markTestSkipped('Test requires PHP 8.3.'); - } - $this->analyse([__DIR__ . '/data/missing-readonly-anonymous-class-property-assign.php'], [ [ 'Class class@anonymous/tests/PHPStan/Rules/Properties/data/missing-readonly-anonymous-class-property-assign.php:10 has an uninitialized readonly property $foo. Assign it in the constructor.', @@ -264,12 +238,9 @@ public function testAnonymousReadonlyClass(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug10523(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-10523.php'], [ [ 'Readonly property Bug10523\MultipleWrites::$userAccount is already assigned.', @@ -278,21 +249,15 @@ public function testBug10523(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug10822(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-10822.php'], []); } + #[RequiresPhp('>= 8.1')] public function testRedeclaredReadonlyProperties(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/redeclare-readonly-property.php'], [ [ 'Readonly property RedeclareReadonlyProperty\B1::$myProp is already assigned.', @@ -307,7 +272,7 @@ public function testRedeclaredReadonlyProperties(): void 70, ], [ - 'Readonly property class@anonymous/tests/PHPStan/Rules/Properties/data/redeclare-readonly-property.php:117::$myProp is already assigned.', + 'Readonly property RedeclareReadonlyProperty\A@anonymous/tests/PHPStan/Rules/Properties/data/redeclare-readonly-property.php:117::$myProp is already assigned.', 121, ], [ @@ -325,12 +290,9 @@ public function testRedeclaredReadonlyProperties(): void ]); } + #[RequiresPhp('>= 8.2')] public function testRedeclaredPropertiesOfReadonlyClass(): void { - if (PHP_VERSION_ID < 80200) { - $this->markTestSkipped('Test requires PHP 8.2.'); - } - $this->analyse([__DIR__ . '/data/redeclare-property-of-readonly-class.php'], [ [ 'Readonly property RedeclarePropertyOfReadonlyClass\B1::$promotedProp is already assigned.', @@ -339,12 +301,9 @@ public function testRedeclaredPropertiesOfReadonlyClass(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug8101(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-8101.php'], [ [ 'Readonly property Bug8101\B::$myProp is already assigned.', @@ -353,12 +312,9 @@ public function testBug8101(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug9863(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-9863.php'], [ [ 'Readonly property Bug9863\ReadonlyChildWithoutIsset::$foo is already assigned.', @@ -375,12 +331,23 @@ public function testBug9863(): void ]); } - public function testBug9864(): void + #[RequiresPhp('>= 8.1')] + public function testBug10048(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } + $this->shouldNarrowMethodScopeFromConstructor = true; + $this->analyse([__DIR__ . '/data/bug-10048.php'], []); + } + #[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 0a0c64ded7..259f973c22 100644 --- a/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php +++ b/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -32,30 +32,21 @@ public function testBug6020(): void $this->analyse([__DIR__ . '/data/bug-6020.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug7109(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/bug-7109.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug5172(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-5172.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug7980(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/../../Analyser/data/bug-7980.php'], []); } @@ -64,17 +55,15 @@ 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 { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-6922.php'], []); } diff --git a/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php index dafac4e5f0..1384576b1d 100644 --- a/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php @@ -2,8 +2,11 @@ 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; /** @@ -16,7 +19,11 @@ class OverridingPropertyRuleTest extends RuleTestCase protected function getRule(): Rule { - return new OverridingPropertyRule(true, $this->reportMaybes); + return new OverridingPropertyRule( + self::getContainer()->getByType(PhpVersion::class), + true, + $this->reportMaybes, + ); } public function testRule(): void @@ -98,7 +105,7 @@ public function testRule(): void ]); } - public function dataRulePHPDocTypes(): array + public static function dataRulePHPDocTypes(): array { $tip = sprintf( "You can fix 3rd party PHPDoc types with stub files:\n %s", @@ -150,9 +157,9 @@ public function dataRulePHPDocTypes(): array } /** - * @dataProvider dataRulePHPDocTypes * @param list $errors */ + #[DataProvider('dataRulePHPDocTypes')] public function testRulePHPDocTypes(bool $reportMaybes, array $errors): void { $this->reportMaybes = $reportMaybes; @@ -171,4 +178,103 @@ public function testBug7692(): void $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, + ], + ]); + } + + #[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 index ecf4597d97..3edbe05c09 100644 --- a/tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php @@ -2,8 +2,11 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -13,20 +16,177 @@ class PropertiesInInterfaceRuleTest extends RuleTestCase protected function getRule(): Rule { - return new PropertiesInInterfaceRule(); + return new PropertiesInInterfaceRule(new PhpVersion(PHP_VERSION_ID)); } - public function testRule(): void + #[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 may not include properties.', + 'Interfaces can include properties only on PHP 8.4 and later.', 7, ], [ - 'Interfaces may not include properties.', + '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 b6d394d30b..e5eab80521 100644 --- a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Properties; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -22,25 +21,25 @@ 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, false, false, true, false), + 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(), ), true, ), diff --git a/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php new file mode 100644 index 0000000000..ef9b16df15 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php @@ -0,0 +1,61 @@ + + */ +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, + ], + ]); + } + +} 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 index d000d4f3f3..5a228e1bb0 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -59,12 +59,9 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1')] public function testRuleIgnoresNativeReadonly(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/readonly-assign-ref-phpdoc-and-native.php'], []); } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php index e5496fc646..adbd30eb43 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -5,7 +5,7 @@ use PHPStan\Reflection\ConstructorsHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -125,24 +125,22 @@ public function testRule(): void '@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 { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/readonly-assign-phpdoc-and-native.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug7361(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-7361.php'], [ [ '@readonly property Bug7361\Example::$foo is assigned outside of the constructor.', @@ -151,13 +149,40 @@ public function testBug7361(): void ]); } + #[RequiresPhp('>= 8.1')] public function testFeature7648(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $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 index c2dce590b6..13d3d988aa 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,12 +17,9 @@ protected function getRule(): Rule return new ReadOnlyByPhpDocPropertyRule(); } + #[RequiresPhp('>= 8.0')] public function testRule(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/read-only-property-phpdoc.php'], [ [ '@readonly property cannot have a default value.', @@ -43,21 +40,15 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1')] public function testRuleIgnoresNativeReadonly(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/read-only-property-phpdoc-and-native.php'], []); } + #[RequiresPhp('>= 8.0')] public function testRuleAllowedPrivateMutation(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/read-only-property-phpdoc-allowed-private-mutation.php'], [ [ '@readonly property cannot have a default value.', diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRefRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRefRuleTest.php index c0dae45d97..4e0d4b4293 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRefRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRefRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use const PHP_VERSION_ID; /** @@ -17,13 +18,10 @@ protected function getRule(): Rule return new ReadOnlyPropertyAssignRefRule(new PropertyReflectionFinder()); } + #[RequiresPhp('>= 8.1')] public function testRule(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - - $this->analyse([__DIR__ . '/data/readonly-assign-ref.php'], [ + $errors = [ [ 'Readonly property ReadOnlyPropertyAssignRef\Foo::$foo is assigned by reference.', 14, @@ -32,11 +30,17 @@ public function testRule(): void 'Readonly property ReadOnlyPropertyAssignRef\Foo::$bar is assigned by reference.', 15, ], - [ + ]; + + if (PHP_VERSION_ID < 80400) { + // reported by PropertyAssignRefRule on 8.4+ + $errors[] = [ 'Readonly property ReadOnlyPropertyAssignRef\Foo::$bar is assigned by reference.', 26, - ], - ]); + ]; + } + + $this->analyse([__DIR__ . '/data/readonly-assign-ref.php'], $errors); } } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php index 90da9c44ec..f5a471f6eb 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Reflection\ConstructorsHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; +use function array_merge; use const PHP_VERSION_ID; /** @@ -26,13 +28,10 @@ protected function getRule(): Rule ); } + #[RequiresPhp('>= 8.1')] public function testRule(): void { - if (PHP_VERSION_ID < 80100) { - self::markTestSkipped('Test requires PHP 8.1'); - } - - $this->analyse([__DIR__ . '/data/readonly-assign.php'], [ + $errors = [ [ 'Readonly property ReadonlyPropertyAssign\Foo::$foo is assigned outside of the constructor.', 21, @@ -49,10 +48,17 @@ public function testRule(): void 'Readonly property ReadonlyPropertyAssign\Foo::$bar is assigned outside of its declaring class.', 39, ], - [ + ]; + + if (PHP_VERSION_ID < 80400) { + // reported by AccessPropertiesInAssignRule on 8.4+ + $errors[] = [ 'Readonly property ReadonlyPropertyAssign\Foo::$baz is assigned outside of its declaring class.', 46, - ], + ]; + } + + $errors = array_merge($errors, [ [ 'Readonly property ReadonlyPropertyAssign\FooArrays::$details is assigned outside of the constructor.', 64, @@ -101,23 +107,31 @@ public function testRule(): void 'Readonly property ReadonlyPropertyAssign\FooEnum::$value is assigned outside of its declaring class.', 152, ],*/ - [ + ]); + + if (PHP_VERSION_ID < 80400) { + // reported by AccessPropertiesInAssignRule on 8.4+ + $errors[] = [ 'Readonly property ReadonlyPropertyAssign\Foo::$baz is assigned outside of its declaring class.', 162, - ], - [ + ]; + $errors[] = [ 'Readonly property ReadonlyPropertyAssign\Foo::$baz is assigned outside of its declaring class.', 163, - ], - ]); + ]; + } + + $errors[] = [ + 'Readonly property ReadonlyPropertyAssign\ArrayAccessPropertyFetch::$storage is assigned outside of the constructor.', + 212, + ]; + + $this->analyse([__DIR__ . '/data/readonly-assign.php'], $errors); } + #[RequiresPhp('>= 8.1')] public function testFeature7648(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/feature-7648.php'], [ [ 'Readonly property Feature7648\Request::$offset is assigned outside of the constructor.', @@ -126,12 +140,9 @@ public function testFeature7648(): void ]); } + #[RequiresPhp('>= 8.1')] public function testReadOnlyClasses(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/readonly-class-assign.php'], [ [ 'Readonly property ReadonlyClassPropertyAssign\Foo::$foo is assigned outside of the constructor.', @@ -140,12 +151,9 @@ public function testReadOnlyClasses(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug6773(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/bug-6773.php'], [ [ 'Readonly property Bug6773\Repository::$data is assigned outside of the constructor.', @@ -154,4 +162,16 @@ public function testBug6773(): void ]); } + #[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 30f9606744..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 @@ -19,7 +20,7 @@ protected function getRule(): Rule return new ReadOnlyPropertyRule(new PhpVersion($this->phpVersionId)); } - public function dataRule(): array + public static function dataRule(): array { return [ [ @@ -80,9 +81,9 @@ public function dataRule(): array } /** - * @dataProvider dataRule * @param list $errors */ + #[DataProvider('dataRule')] public function testRule(int $phpVersionId, array $errors): void { $this->phpVersionId = $phpVersionId; @@ -90,9 +91,9 @@ public function testRule(int $phpVersionId, array $errors): void } /** - * @dataProvider dataRule * @param list $errors */ + #[DataProvider('dataRule')] public function testRuleReadonlyClass(int $phpVersionId, array $errors): void { $this->phpVersionId = $phpVersionId; diff --git a/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php index d5834f4003..1a12b6047c 100644 --- a/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -16,7 +17,7 @@ class ReadingWriteOnlyPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ReadingWriteOnlyPropertiesRule(new PropertyDescriptor(), new PropertyReflectionFinder(), new RuleLevelHelper($this->createReflectionProvider(), true, $this->checkThisOnly, true, false, false, 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 @@ -88,4 +89,20 @@ public function testConflictingAnnotationProperty(): void $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, + ], + ]); + } + } 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/TypesAssignedToPropertiesRuleNoBleedingEdgeTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleNoBleedingEdgeTest.php deleted file mode 100644 index 9b0aaf913d..0000000000 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleNoBleedingEdgeTest.php +++ /dev/null @@ -1,50 +0,0 @@ - - */ -class TypesAssignedToPropertiesRuleNoBleedingEdgeTest extends RuleTestCase -{ - - private bool $checkExplicitMixed = false; - - protected function getRule(): Rule - { - return new TypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false, true, false), new PropertyReflectionFinder()); - } - - public function testGenericObjectWithUnspecifiedTemplateTypes(): void - { - $this->checkExplicitMixed = true; - $this->analyse([__DIR__ . '/data/generic-object-unspecified-template-types.php'], [ - [ - 'Property GenericObjectUnspecifiedTemplateTypes\Bar::$ints (GenericObjectUnspecifiedTemplateTypes\ArrayCollection) does not accept GenericObjectUnspecifiedTemplateTypes\ArrayCollection.', - 67, - ], - ]); - } - - public function testGenericObjectWithUnspecifiedTemplateTypesLevel8(): void - { - $this->checkExplicitMixed = false; - $this->analyse([__DIR__ . '/data/generic-object-unspecified-template-types.php'], [ - [ - 'Property GenericObjectUnspecifiedTemplateTypes\Bar::$ints (GenericObjectUnspecifiedTemplateTypes\ArrayCollection) does not accept GenericObjectUnspecifiedTemplateTypes\ArrayCollection.', - 67, - ], - ]); - } - - public static function getAdditionalConfigFiles(): array - { - // no bleeding edge - return []; - } - -} diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 09f34d1d40..d5a4679aef 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -5,7 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -15,9 +15,11 @@ class TypesAssignedToPropertiesRuleTest extends RuleTestCase private bool $checkExplicitMixed = false; + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { - return new TypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false, true, false), new PropertyReflectionFinder()); + return new TypesAssignedToPropertiesRule(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), new PropertyReflectionFinder()); } public function testTypesAssignedToProperties(): void @@ -107,6 +109,21 @@ public function testTypesAssignedToProperties(): void 'Property PropertiesAssignedTypes\AppendToArrayAccess::$collection2 (ArrayAccess&Countable) does not accept Countable.', 376, ], + [ + 'Property PropertiesAssignedTypes\ParamOutAssign::$foo (list) does not accept string.', + 400, + 'string is not a list.', + ], + [ + 'Property PropertiesAssignedTypes\ParamOutAssign::$foo2 (list>) does not accept string.', + 410, + 'string is not a list.', + ], + [ + 'Property PropertiesAssignedTypes\ParamOutAssign::$foo2 (list>) does not accept non-empty-list|string>.', + 415, + 'list|string might not be a list.', + ], ]); } @@ -143,10 +160,6 @@ public function testTypesAssignedToPropertiesExpressionNames(): void '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, @@ -208,6 +221,29 @@ 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 @@ -292,7 +328,7 @@ public function testAppendedArrayItemType(): void 45, ], [ - 'Property AppendedArrayItem\Baz::$staticProperty (array) does not accept array.', + 'Property AppendedArrayItem\Baz::$staticProperty (array) does not accept array.', 79, ], ], @@ -315,9 +351,6 @@ public function testBug5804(): void public function testBug6286(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->analyse([__DIR__ . '/data/bug-6286.php'], [ [ 'Property Bug6286\HelloWorld::$details (array{name: string, age: int}) does not accept array{name: string, age: \'Forty-two\'}.', @@ -408,19 +441,12 @@ public function testGenericObjectWithUnspecifiedTemplateTypesLevel8(): void public function testBug5382(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->checkExplicitMixed = false; $this->analyse([__DIR__ . '/data/bug-5382.php'], []); } public function testBug6757(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/bug-6757.php'], []); } @@ -506,10 +532,6 @@ public function testIntegerRangesAndConstants(): void public function testBug3311b(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/bug-3311b.php'], [ [ @@ -528,20 +550,12 @@ public function testBug7789(): void public function testBug9131(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/bug-9131.php'], []); } public function testBug8222(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/bug-8222.php'], []); } @@ -614,12 +628,14 @@ public function testGenericsInCallableInConstructor(): void $this->analyse([__DIR__ . '/data/generics-in-callable-in-constructor.php'], []); } - public function testBug11275(): void + public function testBug10686(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } + $this->analyse([__DIR__ . '/data/bug-10686.php'], []); + } + #[RequiresPhp('>= 8.0')] + public function testBug11275(): void + { $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/bug-11275.php'], [ [ @@ -630,4 +646,171 @@ public function testBug11275(): void ]); } + 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'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php index 340c1331d9..940dc461a3 100644 --- a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Reflection\PropertyReflection; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use function strpos; /** @@ -216,4 +217,16 @@ public function testRedeclareReadonlyProperties(): void ]); } + #[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/WritingToReadOnlyPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php index d3196aba89..1dd3766d94 100644 --- a/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -16,7 +17,7 @@ class WritingToReadOnlyPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new WritingToReadOnlyPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, 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 @@ -87,4 +88,27 @@ public function testConflictingAnnotationProperty(): void ]); } + #[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'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook-parse-error.php b/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook-parse-error.php new file mode 100644 index 0000000000..230de9d816 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook-parse-error.php @@ -0,0 +1,10 @@ += 8.4 + +namespace AbstractFinalHookParseError; + +abstract class User +{ + final abstract public string $bar { + get; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook.php b/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook.php new file mode 100644 index 0000000000..baba303bf1 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook.php @@ -0,0 +1,15 @@ += 8.4 + +namespace AbstractFinalHook; + +abstract class User +{ + abstract public string $foo { + final get; + } +} + +abstract class Foo +{ + abstract public int $i { final get { return 1;} set; } +} diff --git a/tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php b/tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php new file mode 100644 index 0000000000..d035d36810 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php @@ -0,0 +1,10 @@ + $this->name; + set => $this->name = $value; + } + + public abstract string $lastName { + get => $this->lastName; + set => $this->lastName = $value; + } + + public abstract string $middleName { + get => $this->name; + set; + } + + public abstract string $familyName { + get; + set; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php b/tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php new file mode 100644 index 0000000000..b34e66a886 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php @@ -0,0 +1,10 @@ += 8.4 + +namespace AbstractPrivateHook; + +abstract class Foo +{ + abstract private int $i { get; } + abstract protected int $ii { get; } + abstract public int $iii { get; } +} diff --git a/tests/PHPStan/Rules/Properties/data/access-properties-after-isnull.php b/tests/PHPStan/Rules/Properties/data/access-properties-after-isnull.php index 434d25be5d..1189bc2232 100644 --- a/tests/PHPStan/Rules/Properties/data/access-properties-after-isnull.php +++ b/tests/PHPStan/Rules/Properties/data/access-properties-after-isnull.php @@ -33,22 +33,22 @@ public function doFoo($foo) } while (is_null($foo) && $foo->fooProperty) { - + break; } while (is_null($foo) || $foo->fooProperty) { - + break; } while (!is_null($foo) && $foo->fooProperty) { - + break; } while (!is_null($foo) || $foo->fooProperty) { - + break; } while (is_null($foo) || $foo->barProperty) { - + break; } while (!is_null($foo) && $foo->barProperty) { - + break; } } diff --git a/tests/PHPStan/Rules/Properties/data/access-properties-assign-op.php b/tests/PHPStan/Rules/Properties/data/access-properties-assign-op.php index 7af71ae1b8..ddac393d08 100644 --- a/tests/PHPStan/Rules/Properties/data/access-properties-assign-op.php +++ b/tests/PHPStan/Rules/Properties/data/access-properties-assign-op.php @@ -1,4 +1,4 @@ -= 7.4 +foo) && isset($m->bar)) { + echo $m->foo; + echo $m->bar; + echo $m->baz; + } + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/access-static-properties-assign-op.php b/tests/PHPStan/Rules/Properties/data/access-static-properties-assign-op.php index 0cf096c2e1..9d065b72f3 100644 --- a/tests/PHPStan/Rules/Properties/data/access-static-properties-assign-op.php +++ b/tests/PHPStan/Rules/Properties/data/access-static-properties-assign-op.php @@ -1,4 +1,4 @@ -= 7.4 += 8.4 + +namespace AssignHookedProperties; + +class Foo +{ + + public int $i { + /** @param array|int $val */ + set (array|int $val) { + $this->i = $val; // only int allowed + } + } + + public int $j { + /** @param array|int $val */ + set (array|int $val) { + $this->i = $val; // this is okay - hook called + $this->j = $val; // only int allowed + } + } + + public function doFoo(): void + { + $this->i = ['foo']; // okay + $this->i = 1; // okay + $this->i = [1]; // not okay + } + +} + +/** + * @template T + */ +class FooGenerics +{ + + /** @var T */ + public $a { + set { + $this->a = $value; + } + } + + /** + * @param FooGenerics $f + * @return void + */ + public static function doFoo(self $f): void + { + $f->a = 1; + $f->a = 'foo'; + } + + /** + * @param T $t + */ + public function doBar($t): void + { + $this->a = $t; + $this->a = 1; + } + +} + +/** + * @template T + */ +class FooGenericsParam +{ + + /** @var array */ + public array $a { + /** @param array|int $value */ + set (array|int $value) { + $this->a = $value; // not ok + + if (is_array($value)) { + $this->a = $value; // ok + } + } + } + + /** + * @param FooGenericsParam $f + * @return void + */ + public static function doFoo(self $f): void + { + $f->a = [1]; // ok + $f->a = ['foo']; // not ok + } + + /** + * @param T $t + */ + public function doBar($t): void + { + $this->a = [$t]; // ok + $this->a = 1; // ok + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-10048.php b/tests/PHPStan/Rules/Properties/data/bug-10048.php new file mode 100644 index 0000000000..d537fb6527 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-10048.php @@ -0,0 +1,26 @@ += 8.1 + +namespace Bug10048; + +class Foo { + private readonly string $bar; + private readonly \Closure $callback; + public function __construct() { + $this->bar = "hi"; + $this->useBar(); + echo $this->bar; + $this->callback = function() { + $this->useBar(); + }; + } + + private function useBar(): void { + echo $this->bar; + } + + public function useCallback(): void { + call_user_func($this->callback); + } +} + +(new Foo())->useCallback(); diff --git a/tests/PHPStan/Rules/Properties/data/bug-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-11171.php b/tests/PHPStan/Rules/Properties/data/bug-11171.php new file mode 100644 index 0000000000..688e1c501c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-11171.php @@ -0,0 +1,41 @@ + + */ + 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-11275.php b/tests/PHPStan/Rules/Properties/data/bug-11275.php index cf54868d05..42b1333560 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-11275.php +++ b/tests/PHPStan/Rules/Properties/data/bug-11275.php @@ -1,4 +1,4 @@ -= 7.4 + + */ + private $params; + + public function sayHello(string $query): void + { + \parse_str($query, $this->params); + \parse_str($query, $tmp); + $this->params = $tmp; + + /** @var array $foo */ + $foo = []; + \parse_str($query, $foo); + $this->params = $foo; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-11828.php b/tests/PHPStan/Rules/Properties/data/bug-11828.php new file mode 100644 index 0000000000..0a030d7bd0 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-11828.php @@ -0,0 +1,31 @@ += 8.1 + +namespace Bug11828; + +class Dummy +{ + /** + * @var callable + */ + private $callable; + private readonly int $foo; + + public function __construct(int $foo) + { + $this->foo = $foo; + + $this->callable = function () { + $foo = $this->getFoo(); + }; + } + + public function getFoo(): int + { + return $this->foo; + } + + public function getCallable(): callable + { + return $this->callable; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-12131.php b/tests/PHPStan/Rules/Properties/data/bug-12131.php new file mode 100755 index 0000000000..6f7f8d83d8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12131.php @@ -0,0 +1,31 @@ += 7.4 + +namespace Bug12131; + +class Test +{ + /** + * @var non-empty-list + */ + public array $array; + + public function __construct() + { + $this->array = array_fill(0, 10, 1); + } + + public function setAtZero(): void + { + $this->array[0] = 1; + } + + public function setAtOne(): void + { + $this->array[1] = 1; + } + + public function setAtTwo(): void + { + $this->array[2] = 1; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-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-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-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-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-3311b.php b/tests/PHPStan/Rules/Properties/data/bug-3311b.php index 93464208a8..30e0eed390 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-3311b.php +++ b/tests/PHPStan/Rules/Properties/data/bug-3311b.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 + + */ + 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-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-5382.php b/tests/PHPStan/Rules/Properties/data/bug-5382.php index b75b4ab167..7ec41886da 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-5382.php +++ b/tests/PHPStan/Rules/Properties/data/bug-5382.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 += 7.4 += 7.4 +>|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-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 index a695ce2a68..1c5e30f734 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-6757.php +++ b/tests/PHPStan/Rules/Properties/data/bug-6757.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 += 7.4 + */ + 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 index 8337f13753..4437eb7d83 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-7933.php +++ b/tests/PHPStan/Rules/Properties/data/bug-7933.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 += 7.4 += 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-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-9131.php b/tests/PHPStan/Rules/Properties/data/bug-9131.php index 073e27a0d7..e636ede694 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-9131.php +++ b/tests/PHPStan/Rules/Properties/data/bug-9131.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 +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/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 index 055029152e..d354c028ea 100644 --- a/tests/PHPStan/Rules/Properties/data/dynamic-properties.php +++ b/tests/PHPStan/Rules/Properties/data/dynamic-properties.php @@ -15,6 +15,12 @@ public function doBar() { empty($bar->dynamicProperty); $bar->dynamicProperty ?? 'test'; } + + public function doBaz(Bar $bar) { + isset($bar->dynamicProperty); + empty($bar->dynamicProperty); + $bar->dynamicProperty ?? 'test'; + } } #[\AllowDynamicProperties] 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 index ce3c3226e2..cc205ab42c 100644 --- a/tests/PHPStan/Rules/Properties/data/efabrica-latte-bug.php +++ b/tests/PHPStan/Rules/Properties/data/efabrica-latte-bug.php @@ -1,4 +1,4 @@ -= 7.4 += 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/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/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 @@ += 7.4 +bar ?? 'no'; +}; 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 @@ += 7.4 += 7.4 +world)) + { + echo $hello->world; + } +}; + final class FinalHelloWorld { public function __get(string $attribute): mixed diff --git a/tests/PHPStan/Rules/Properties/data/private-final-property-hooks.php b/tests/PHPStan/Rules/Properties/data/private-final-property-hooks.php new file mode 100644 index 0000000000..1ce2830a95 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/private-final-property-hooks.php @@ -0,0 +1,36 @@ += 8.4 + +namespace PrivateFinalHook; + +final class User +{ + final private string $privatePropGet = 'mailto: example.org' { + get => 'private:' . $this->privatePropGet; + } + + private string $private = 'mailto: example.org' { + final set => 'private:' . $this->private; + get => 'private:' . $this->private; + } + + protected string $protected = 'mailto: example.org' { + final get => 'protected:' . $this->protected; + } + + public string $public = 'mailto: example.org' { + final get => 'public:' . $this->public; + } + + private string $email = 'mailto: example.org' { + get => 'mailto:' . $this->email; + } + + function doFoo(): void + { + $u = new User; + var_dump($u->private); + var_dump($u->protected); + var_dump($u->public); + var_dump($u->email); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/properties-assigned-types.php b/tests/PHPStan/Rules/Properties/data/properties-assigned-types.php index 3686d4d244..14b3ad66e4 100644 --- a/tests/PHPStan/Rules/Properties/data/properties-assigned-types.php +++ b/tests/PHPStan/Rules/Properties/data/properties-assigned-types.php @@ -376,3 +376,48 @@ public function foo(): void $this->collection2[] = 2; } } + +class ParamOutAssign +{ + + /** @var list */ + private $foo; + + /** @var list> */ + private $foo2; + + /** + * @param mixed $a + * @param-out string $a + */ + public function paramOut(&$a): void + { + + } + + public function doFoo(): void + { + $this->paramOut($this->foo); + } + + public function doFoo2(): void + { + $this->paramOut($this->foo[0]); + } + + public function doBar(): void + { + $this->paramOut($this->foo2); + } + + public function doBar2(): void + { + $this->paramOut($this->foo2[0]); + } + + public function doBar3(): void + { + $this->paramOut($this->foo2[0][0]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/properties-in-interface.php b/tests/PHPStan/Rules/Properties/data/properties-in-interface.php index 4e897d1998..4d104487fb 100644 --- a/tests/PHPStan/Rules/Properties/data/properties-in-interface.php +++ b/tests/PHPStan/Rules/Properties/data/properties-in-interface.php @@ -4,7 +4,11 @@ interface HelloWorld { + public string $name { get; } + public \DateTimeInterface $dateTime; public static \Closure $callable; + + public final \DateTime $finalProperty; } diff --git a/tests/PHPStan/Rules/Properties/data/properties-native-types.php b/tests/PHPStan/Rules/Properties/data/properties-native-types.php index 15eadd944d..32ce72b183 100644 --- a/tests/PHPStan/Rules/Properties/data/properties-native-types.php +++ b/tests/PHPStan/Rules/Properties/data/properties-native-types.php @@ -1,4 +1,4 @@ -= 7.4 += 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-exists.php b/tests/PHPStan/Rules/Properties/data/property-exists.php new file mode 100644 index 0000000000..cb7e998027 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-exists.php @@ -0,0 +1,29 @@ +{$column}; + } + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/property-hook-attributes.php b/tests/PHPStan/Rules/Properties/data/property-hook-attributes.php new file mode 100644 index 0000000000..495cc793b0 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-hook-attributes.php @@ -0,0 +1,57 @@ += 8.4 + +namespace PropertyHookAttributes; + +#[\Attribute(\Attribute::TARGET_CLASS)] +class Foo +{ + +} + +#[\Attribute(\Attribute::TARGET_METHOD)] +class Bar +{ + +} + +#[\Attribute(\Attribute::TARGET_ALL)] +class Baz +{ + +} + +class Lorem +{ + + public int $i { + #[Foo] + get { + + } + } + +} + +class Ipsum +{ + + public int $i { + #[Bar] + get { + + } + } + +} + +class Dolor +{ + + public int $i { + #[Baz] + get { + + } + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/property-hooks-bodies-in-interface.php b/tests/PHPStan/Rules/Properties/data/property-hooks-bodies-in-interface.php new file mode 100644 index 0000000000..58af100248 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-hooks-bodies-in-interface.php @@ -0,0 +1,20 @@ += 8.4 + +namespace PropertyHooksReadonlyByPhpDocAssign; + +class Foo +{ + + /** @readonly */ + public int $i { + get { + return $this->i + 1; + } + set { + $self = new self(); + $self->i = 1; + + $this->j = 2; + $this->i = $value - 1; + } + } + + /** @readonly */ + public int $j; + +} diff --git a/tests/PHPStan/Rules/Properties/data/property-hooks-visibility-in-interface.php b/tests/PHPStan/Rules/Properties/data/property-hooks-visibility-in-interface.php new file mode 100644 index 0000000000..cd70c3b514 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-hooks-visibility-in-interface.php @@ -0,0 +1,12 @@ += 8.4 + +namespace Bug12466; + +interface Foo +{ + + public int $a { get; set;} + +} + +class Bar implements Foo +{ + + public string $a; + +} + +interface MoreProps +{ + + public int $a { get; set; } + + public int $b { get; } + + public int $c { set; } + +} + +class TestMoreProps implements MoreProps +{ + + // not writable + public int $a { + get { + return 1; + } + } + + // not readable + public int $b { + set { + $this->a = 1; + } + } + + // not writable + public int $c { + get { + return 1; + } + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/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/reading-write-only-hooked-properties.php b/tests/PHPStan/Rules/Properties/data/reading-write-only-hooked-properties.php new file mode 100644 index 0000000000..9f695e4573 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/reading-write-only-hooked-properties.php @@ -0,0 +1,51 @@ += 8.4 + +namespace ReadingWriteOnlyHookedProperties; + +interface Foo +{ + + public int $i { + // virtual, not readable + set; + } + +} + +function (Foo $f): void { + echo $f->i; +}; + +class Bar +{ + + public int $other; + + public int $i { + // virtual, not readable + set { + $this->other = 1; + } + } + +} + +function (Bar $b): void { + echo $b->i; +}; + +class Baz +{ + + public int $i { + // backed, readable + set { + $this->i = 1; + } + } + +} + +function (Baz $b): void { + $b->i = 1; +}; diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php b/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php index 55af82fe30..c390bbb6da 100644 --- a/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php @@ -294,3 +294,21 @@ public function mod() } } + +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.php b/tests/PHPStan/Rules/Properties/data/readonly-assign.php index fe4af36466..e23655217c 100644 --- a/tests/PHPStan/Rules/Properties/data/readonly-assign.php +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign.php @@ -196,3 +196,20 @@ protected function setUp(): void } } + +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-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/require-extends.php b/tests/PHPStan/Rules/Properties/data/require-extends.php index 14271b4ef6..a9d22d0eb5 100644 --- a/tests/PHPStan/Rules/Properties/data/require-extends.php +++ b/tests/PHPStan/Rules/Properties/data/require-extends.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 += 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 index 7e3cda91a5..8af919ecb3 100644 --- a/tests/PHPStan/Rules/Properties/data/uninitialized-property-additional-constructors.php +++ b/tests/PHPStan/Rules/Properties/data/uninitialized-property-additional-constructors.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 += 7.4 + $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/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/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index c310f6177c..f0f39b2b5e 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -104,15 +104,36 @@ public function testRule(): void '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 { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->analyse([__DIR__ . '/data/first-class-callable-pure-function.php'], [ [ 'Impure call to method FirstClassCallablePureFunction\Foo::impureFunction() in pure function FirstClassCallablePureFunction\testThese().', @@ -167,4 +188,25 @@ public function testBug11361(): void ]); } + 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 index 4ec5028172..b5a44ee822 100644 --- a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php @@ -4,7 +4,8 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -140,17 +141,26 @@ public function testRule(): void '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 { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/pure-constructor.php'], [ + [ + 'Impure static property access in pure method PureConstructor\Foo::__construct().', + 19, + ], [ 'Impure property assignment in pure method PureConstructor\Foo::__construct().', 19, @@ -177,16 +187,14 @@ public function testImpureAssignRef(): void ]); } - /** - * @dataProvider dataBug11207 - */ + #[DataProvider('dataBug11207')] public function testBug11207(bool $treatPhpDocTypesAsCertain): void { $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; $this->analyse([__DIR__ . '/data/bug-11207.php'], []); } - public function dataBug11207(): array + public static function dataBug11207(): array { return [ [true], @@ -194,4 +202,41 @@ public function dataBug11207(): array ]; } + 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-12048.php b/tests/PHPStan/Rules/Pure/data/bug-12048.php new file mode 100644 index 0000000000..ab5d44154a --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-12048.php @@ -0,0 +1,19 @@ +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/impure-assign-ref.php b/tests/PHPStan/Rules/Pure/data/impure-assign-ref.php index 7aca03a4a4..15b1dac588 100644 --- a/tests/PHPStan/Rules/Pure/data/impure-assign-ref.php +++ b/tests/PHPStan/Rules/Pure/data/impure-assign-ref.php @@ -1,4 +1,4 @@ -= 7.4 + 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 index bcaa939b76..eca2976cda 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-method.php +++ b/tests/PHPStan/Rules/Pure/data/pure-method.php @@ -2,7 +2,7 @@ namespace PureMethod; -class Foo +final class Foo { /** @@ -92,7 +92,7 @@ public function doFoo5() } -class PureConstructor +final class PureConstructor { /** @@ -105,7 +105,7 @@ public function __construct() } -class ImpureConstructor +final class ImpureConstructor { /** @@ -118,7 +118,7 @@ public function __construct() } -class PossiblyImpureConstructor +final class PossiblyImpureConstructor { public function __construct() @@ -128,7 +128,7 @@ public function __construct() } -class TestConstructors +final class TestConstructors { /** @@ -144,7 +144,7 @@ public function doFoo(string $s) } -class ActuallyPure +final class ActuallyPure { /** @@ -175,7 +175,7 @@ public function impure(): int } -class ExtendingClass extends ToBeExtended +final class ExtendingClass extends ToBeExtended { public function pure(): int @@ -191,7 +191,7 @@ public function impure(): int } -class ClassWithVoidMethods +final class ClassWithVoidMethods { public function voidFunctionThatThrows(): void @@ -235,12 +235,12 @@ public function purePostGetAssign(array $post = [], array $get = []): int } -class NoMagicMethods +final class NoMagicMethods { } -class PureMagicMethods +final class PureMagicMethods { /** @@ -253,7 +253,7 @@ public function __toString(): string } -class MaybePureMagicMethods +final class MaybePureMagicMethods { public function __toString(): string @@ -263,7 +263,7 @@ public function __toString(): string } -class ImpureMagicMethods +final class ImpureMagicMethods { /** @@ -277,7 +277,7 @@ public function __toString(): string } -class TestMagicMethods +final class TestMagicMethods { /** @@ -298,12 +298,12 @@ public function doFoo( } -class NoConstructor +final class NoConstructor { } -class TestNoConstructor +final class TestNoConstructor { /** @@ -318,7 +318,7 @@ public function doFoo(): int } -class MaybeCallableFromUnion +final class MaybeCallableFromUnion { /** @@ -334,7 +334,7 @@ public function doFoo($p): int } -class VoidMethods +final class VoidMethods { private function doFoo(): void @@ -361,7 +361,7 @@ private function doBaz(): void } -class AssertingImpureVoidMethod +final class AssertingImpureVoidMethod { /** @@ -375,3 +375,78 @@ 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 b1c964fada..00f0db3ce3 100644 --- a/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php +++ b/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php @@ -5,6 +5,7 @@ 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; @@ -21,141 +22,15 @@ protected function getRule(): Rule ); } - public function testValidRegexPatternBefore73(): void + public function testValidRegexPattern(): 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 ) 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 ) 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 ) 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 ) 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 ) 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 ) at offset 1 in pattern: ~(~', - 29, - ], - [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', - 32, - ], - [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', - 33, - ], - [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', - 35, - ], - [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', - 35, - ], - [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', - 38, - ], - [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', - 39, - ], - [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', - 41, - ], - [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', - 41, - ], - [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', - 43, - ], - [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', - 43, - ], - [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok(?:.*)', - 57, - ], - [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok(?:.*)', - 58, - ], - [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 7 in pattern: ~((?:.*)~', - 59, - ], - [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok(?:.*)nono', - 61, - ], - [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok(?:.*)nope', - 62, - ], - [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 7 in pattern: ~((?:.*)~', - 63, - ], - ], - ); - } - - public function testValidRegexPatternAfter73(): 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'], @@ -278,8 +153,8 @@ public function testValidRegexPatternAfter73(): void /** * @param list $errors - * @dataProvider dataArrayShapePatterns */ + #[DataProvider('dataArrayShapePatterns')] public function testArrayShapePatterns(string $file, array $errors): void { $this->analyse( @@ -288,7 +163,7 @@ public function testArrayShapePatterns(string $file, array $errors): void ); } - public function dataArrayShapePatterns(): iterable + public static function dataArrayShapePatterns(): iterable { yield [ __DIR__ . '/../../Analyser/nsrt/preg_match_all_shapes.php', diff --git a/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php b/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php index 38197c1aa6..5616a2bfc4 100644 --- a/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php +++ b/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php @@ -5,7 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Regex\RegexExpressionHelper; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -16,7 +16,7 @@ class RegularExpressionQuotingRuleTest extends RuleTestCase protected function getRule(): Rule { return new RegularExpressionQuotingRule( - $this->createReflectionProvider(), + self::createReflectionProvider(), self::getContainer()->getByType(RegexExpressionHelper::class), ); } @@ -74,12 +74,9 @@ public function testRule(): void ); } + #[RequiresPhp('>= 8.0')] public function testRulePhp8(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse( [__DIR__ . '/data/preg-quote-php8.php'], [ 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 @@ + @@ -16,12 +16,9 @@ protected function getRule(): Rule return new ScopeFunctionCallStackRule(); } + #[RequiresPhp('>= 8.0')] public function testRule(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/scope-function-call-stack.php'], [ [ "var_dump\nprint_r\nsleep", diff --git a/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRuleTest.php b/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRuleTest.php index 38a0aecd61..b784adf647 100644 --- a/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRuleTest.php +++ b/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRuleTest.php @@ -3,7 +3,7 @@ namespace PHPStan\Rules; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -16,12 +16,9 @@ protected function getRule(): Rule return new ScopeFunctionCallStackWithParametersRule(); } + #[RequiresPhp('>= 8.0')] public function testRule(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/scope-function-call-stack.php'], [ [ "var_dump (\$value)\nprint_r (\$value)\nsleep (\$seconds)", diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php index b914db3141..9c86812e92 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php @@ -51,4 +51,14 @@ public function testRule(): void ]); } + public function testBug11980(): void + { + $this->analyse([__DIR__ . '/data/bug-11980-function.php'], [ + [ + 'Function Bug11980Function\process2() never returns void so it can be removed from the return type.', + 34, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php index 9e9588d219..2d84f5dff5 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php @@ -4,7 +4,8 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -14,11 +15,9 @@ class TooWideMethodReturnTypehintRuleTest extends RuleTestCase private bool $checkProtectedAndPublicMethods = true; - private bool $alwaysCheckFinal = false; - protected function getRule(): Rule { - return new TooWideMethodReturnTypehintRule($this->checkProtectedAndPublicMethods, $this->alwaysCheckFinal); + return new TooWideMethodReturnTypehintRule($this->checkProtectedAndPublicMethods); } public function testPrivate(): void @@ -97,11 +96,9 @@ public function testBug5095(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug6158(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->analyse([__DIR__ . '/data/bug-6158.php'], []); } @@ -110,29 +107,9 @@ public function testBug6175(): void $this->analyse([__DIR__ . '/data/bug-6175.php'], []); } - public function dataAlwaysCheckFinal(): iterable + public static function dataAlwaysCheckFinal(): iterable { yield [ - false, - false, - [ - [ - 'Method MethodTooWideReturnAlwaysCheckFinal\Foo::test() never returns null so it can be removed from the return type.', - 8, - ], - [ - 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test() never returns null so it can be removed from the return type.', - 28, - ], - [ - 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test() never returns null so it can be removed from the return type.', - 48, - ], - ], - ]; - - yield [ - true, false, [ [ @@ -167,42 +144,6 @@ public function dataAlwaysCheckFinal(): iterable ]; yield [ - false, - true, - [ - [ - 'Method MethodTooWideReturnAlwaysCheckFinal\Foo::test() never returns null so it can be removed from the return type.', - 8, - ], - [ - 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test() never returns null so it can be removed from the return type.', - 28, - ], - [ - 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test2() never returns null so it can be removed from the return type.', - 33, - ], - [ - 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test3() never returns null so it can be removed from the return type.', - 38, - ], - [ - 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test() never returns null so it can be removed from the return type.', - 48, - ], - [ - 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test2() never returns null so it can be removed from the return type.', - 53, - ], - [ - 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test3() never returns null so it can be removed from the return type.', - 58, - ], - ], - ]; - - yield [ - true, true, [ [ @@ -238,14 +179,24 @@ public function dataAlwaysCheckFinal(): iterable } /** - * @dataProvider dataAlwaysCheckFinal * @param list $expectedErrors */ - public function testAlwaysCheckFinal(bool $checkProtectedAndPublicMethods, bool $alwaysCheckFinal, array $expectedErrors): void + #[DataProvider('dataAlwaysCheckFinal')] + public function testAlwaysCheckFinal(bool $checkProtectedAndPublicMethods, array $expectedErrors): void { $this->checkProtectedAndPublicMethods = $checkProtectedAndPublicMethods; - $this->alwaysCheckFinal = $alwaysCheckFinal; $this->analyse([__DIR__ . '/data/method-too-wide-return-always-check-final.php'], $expectedErrors); } + public function testBug11980(): void + { + $this->checkProtectedAndPublicMethods = true; + $this->analyse([__DIR__ . '/data/bug-11980.php'], [ + [ + 'Method Bug11980\Demo::process2() never returns void so it can be removed from the return type.', + 37, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php new file mode 100644 index 0000000000..213d98342d --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php @@ -0,0 +1,61 @@ + + */ +class TooWidePropertyTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new TooWidePropertyTypeRule( + new DirectReadWritePropertiesExtensionProvider([]), + new PropertyReflectionFinder(), + ); + } + + #[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'], []); + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-10684.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10684.php index 1949d4d8b6..4335dabd72 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/data/bug-10684.php +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10684.php @@ -1,4 +1,4 @@ -= 7.4 +|null */ + private $matches = null; + + public function match(string $string): void { + preg_match('/Hello (\w+)/', $string, $this->matches); + } + + /** @return list|null */ + public function get(): ?array { + return $this->matches; + } +} + +final class HelloWorld2 { + /** @var list|null */ + private $matches = null; + + public function match(string $string): void { + $this->paramOut($this->matches); + } + + /** + * @param mixed $a + * @param-out list $a + */ + public function paramOut(&$a): void + { + + } + + /** @return list|null */ + public function get(): ?array { + return $this->matches; + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980-function.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980-function.php new file mode 100644 index 0000000000..aaafea889d --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980-function.php @@ -0,0 +1,70 @@ +> $tokens + * @param int $stackPtr + * + * @return int|void + */ +function process($tokens, $stackPtr) +{ + if (empty($tokens[$stackPtr]['nested_parenthesis']) === false) { + // Not a stand-alone statement. + return; + } + + $end = 10; + + if ($tokens[$end]['code'] !== 10 + && $tokens[$end]['code'] !== 20 + ) { + // Not a stand-alone statement. + return $end; + } +} + +/** + * @param array> $tokens + * @param int $stackPtr + * + * @return int|void + */ +function process2($tokens, $stackPtr) +{ + if (empty($tokens[$stackPtr]['nested_parenthesis']) === false) { + // Not a stand-alone statement. + return null; + } + + $end = 10; + + if ($tokens[$end]['code'] !== 10 + && $tokens[$end]['code'] !== 20 + ) { + // Not a stand-alone statement. + return $end; + } + + return 1; +} + +/** @return int|void */ +function process3( int $code ) { + + if ( $code === \T_CLASS ) { + return process_class( $code ); + } + + process_function( $code ); +} + +/** @return int */ +function process_class(int $code) { + return $code; +} + +/** @return void */ +function process_function(int $code) { +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980.php new file mode 100644 index 0000000000..d45bb08576 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980.php @@ -0,0 +1,74 @@ +> $tokens + * @param int $stackPtr + * + * @return int|void + */ + public function process($tokens, $stackPtr) + { + if (empty($tokens[$stackPtr]['nested_parenthesis']) === false) { + // Not a stand-alone statement. + return; + } + + $end = 10; + + if ($tokens[$end]['code'] !== 10 + && $tokens[$end]['code'] !== 20 + ) { + // Not a stand-alone statement. + return $end; + } + } + + /** + * @param array> $tokens + * @param int $stackPtr + * + * @return int|void + */ + public function process2($tokens, $stackPtr) + { + if (empty($tokens[$stackPtr]['nested_parenthesis']) === false) { + // Not a stand-alone statement. + return null; + } + + $end = 10; + + if ($tokens[$end]['code'] !== 10 + && $tokens[$end]['code'] !== 20 + ) { + // Not a stand-alone statement. + return $end; + } + + return 1; + } + + /** @return int|void */ + public function process3( int $code ) { + + if ( $code === \T_CLASS ) { + return $this->process_class( $code ); + } + + $this->process_function( $code ); + } + + /** @return int */ + public function process_class(int $code) { + return $code; + } + + /** @return void */ + public function process_function(int $code) { + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-6158.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-6158.php index 3dd1798fc2..0bcc2f80df 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/data/bug-6158.php +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-6158.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 += 8.0 + +namespace TooWidePropertyType; + +class Foo +{ + + /** @var int|string */ + private $foo; + + /** @var int|null */ + private $bar; // do not report "null" as not assigned + + /** @var int|null */ + private $barr = 1; // report "null" as not assigned + + /** @var int|null */ + private $barrr; // assigned in constructor - report "null" as not assigned + + private int|null $baz; // report "null" as not assigned + + public function __construct() + { + $this->barrr = 1; + } + + public function doFoo(): void + { + $this->foo = 1; + $this->bar = 1; + $this->barr = 1; + $this->barrr = 1; + $this->baz = 1; + } + +} + +class Bar +{ + + private ?int $a = null; + + private ?int $b = 1; + + private ?int $c = null; + + private ?int $d = 1; + + public function doFoo(): void + { + $this->a = 1; + $this->b = null; + } + +} + +class Baz +{ + + private ?int $a = null; + + public function doFoo(): self + { + $s = new self(); + $s->a = 1; + + return $s; + } + +} + +class Lorem +{ + + public function __construct( + private ?int $a = null + ) + { + + } + + public function doFoo(): void + { + $this->a = 1; + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/tooWideArrowFunctionReturnType.php b/tests/PHPStan/Rules/TooWideTypehints/data/tooWideArrowFunctionReturnType.php index 632430bdc9..11cfd16fdc 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/data/tooWideArrowFunctionReturnType.php +++ b/tests/PHPStan/Rules/TooWideTypehints/data/tooWideArrowFunctionReturnType.php @@ -1,4 +1,4 @@ -= 7.4 +getByType(InitializerExprTypeResolver::class)); + return new ConflictingTraitConstantsRule(self::getContainer()->getByType(InitializerExprTypeResolver::class), self::createReflectionProvider()); } public function testRule(): void @@ -59,12 +59,9 @@ public function testRule(): void ]); } + #[\PHPUnit\Framework\Attributes\RequiresPhp('>= 8.3')] public function testNativeTypes(): void { - if (PHP_VERSION_ID < 80300) { - $this->markTestSkipped('Test requires PHP 8.3.'); - } - $this->analyse([__DIR__ . '/data/conflicting-trait-constants-types.php'], [ [ 'Constant ConflictingTraitConstantsTypes\Baz::FOO_CONST (int) overriding constant ConflictingTraitConstantsTypes\Foo::FOO_CONST (int|string) should have the same native type int|string.', diff --git a/tests/PHPStan/Rules/Traits/ConstantsInTraitsRuleTest.php b/tests/PHPStan/Rules/Traits/ConstantsInTraitsRuleTest.php index 3b6f591527..d8f5746cd3 100644 --- a/tests/PHPStan/Rules/Traits/ConstantsInTraitsRuleTest.php +++ b/tests/PHPStan/Rules/Traits/ConstantsInTraitsRuleTest.php @@ -20,7 +20,7 @@ protected function getRule(): Rule return new ConstantsInTraitsRule(new PhpVersion($this->phpVersionId)); } - public function dataRule(): array + public static function dataRule(): array { return [ [ @@ -44,11 +44,10 @@ public function dataRule(): array } /** - * @dataProvider dataRule - * - * @param list $errors - */ - public function testRule(int $phpVersionId, array $errors): void + * @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/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/trait-attributes.php b/tests/PHPStan/Rules/Traits/data/trait-attributes.php new file mode 100644 index 0000000000..67906a2dfe --- /dev/null +++ b/tests/PHPStan/Rules/Traits/data/trait-attributes.php @@ -0,0 +1,30 @@ + @@ -30,9 +31,7 @@ public function testRuleOnUnionWithVoid(): void ]); } - /** - * @requires PHP 8.0 - */ + #[RequiresPhp('8.0')] public function testRuleOnUnionWithMixed(): void { $this->analyse([__DIR__ . '/data/invalid-union-with-mixed.php'], [ @@ -71,9 +70,7 @@ public function testRuleOnUnionWithMixed(): void ]); } - /** - * @requires PHP 8.1 - */ + #[RequiresPhp('8.1')] public function testRuleOnUnionWithNever(): void { $this->analyse([__DIR__ . '/data/invalid-union-with-never.php'], [ diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 94f0b0ffeb..96b89cf070 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -4,7 +4,8 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -313,7 +314,7 @@ public function testCliArgumentsVariablesRegistered(): void ]); } - public function dataLoopInitialAssignments(): array + public static function dataLoopInitialAssignments(): array { return [ [ @@ -349,9 +350,9 @@ public function dataLoopInitialAssignments(): array } /** - * @dataProvider dataLoopInitialAssignments * @param list $expectedErrors */ + #[DataProvider('dataLoopInitialAssignments')] public function testLoopInitialAssignments( bool $polluteScopeWithLoopInitialAssignments, bool $checkMaybeUndefinedVariables, @@ -462,7 +463,7 @@ public function testForeach(): void ]); } - public function dataForeachPolluteScopeWithAlwaysIterableForeach(): array + public static function dataForeachPolluteScopeWithAlwaysIterableForeach(): array { return [ [ @@ -571,10 +572,9 @@ public function dataForeachPolluteScopeWithAlwaysIterableForeach(): array } /** - * @dataProvider dataForeachPolluteScopeWithAlwaysIterableForeach - * * @param list $errors */ + #[DataProvider('dataForeachPolluteScopeWithAlwaysIterableForeach')] public function testForeachPolluteScopeWithAlwaysIterableForeach(bool $polluteScopeWithAlwaysIterableForeach, array $errors): void { $this->cliArgumentsVariablesRegistered = true; @@ -877,12 +877,9 @@ public function testBug1016b(): void $this->analyse([__DIR__ . '/data/bug-1016b.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug8142(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; @@ -958,12 +955,9 @@ public function testBug393(): void $this->analyse([__DIR__ . '/data/bug-393.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug9474(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; @@ -971,12 +965,9 @@ public function testBug9474(): void $this->analyse([__DIR__ . '/data/bug-9474.php'], []); } + #[RequiresPhp('>= 8.1')] public function testEnum(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; @@ -1029,12 +1020,9 @@ public function testDiscussion10252(): void $this->analyse([__DIR__ . '/data/discussion-10252.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug10418(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; @@ -1042,11 +1030,9 @@ public function testBug10418(): void $this->analyse([__DIR__ . '/data/bug-10418.php'], []); } + #[RequiresPhp('>= 8.0')] public function testPassByReferenceIntoNotNullable(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; @@ -1068,4 +1054,119 @@ public function testBug10228(): void $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 6fa229669f..f0c0343340 100644 --- a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php +++ b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php @@ -7,7 +7,8 @@ use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,8 +18,6 @@ class EmptyRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; - private bool $strictUnnecessaryNullsafePropertyFetch; - protected function getRule(): Rule { return new EmptyRule(new IssetCheck( @@ -26,7 +25,6 @@ protected function getRule(): Rule new PropertyReflectionFinder(), true, $this->treatPhpDocTypesAsCertain, - $this->strictUnnecessaryNullsafePropertyFetch, )); } @@ -35,10 +33,14 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; + } + public function testRule(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/empty-rule.php'], [ [ 'Offset \'nonexistent\' on array{2: bool, 3: false, 4: true}|array{bool, false, bool, false, true} in empty() does not exist.', @@ -78,7 +80,6 @@ public function testRule(): void public function testBug970(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/bug-970.php'], [ [ 'Variable $ar in empty() is never defined.', @@ -90,7 +91,6 @@ public function testBug970(): void public function testBug6974(): void { $this->treatPhpDocTypesAsCertain = false; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/bug-6974.php'], [ [ 'Variable $a in empty() always exists and is always falsy.', @@ -102,7 +102,6 @@ public function testBug6974(): void public function testBug6974TreatPhpDocTypesAsCertain(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/bug-6974.php'], [ [ 'Variable $a in empty() always exists and is always falsy.', @@ -115,31 +114,10 @@ public function testBug6974TreatPhpDocTypesAsCertain(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug7109(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - - $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; - - $this->analyse([__DIR__ . '/../Properties/data/bug-7109.php'], [ - [ - 'Expression in empty() is not falsy.', - 59, - ], - ]); - } - - public function testBug7109Strict(): void - { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = true; $this->analyse([__DIR__ . '/../Properties/data/bug-7109.php'], [ [ @@ -176,7 +154,6 @@ public function testBug7109Strict(): void public function testBug7318(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = true; $this->analyse([__DIR__ . '/../Properties/data/bug-7318.php'], []); } @@ -184,7 +161,6 @@ public function testBug7318(): void public function testBug7424(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/bug-7424.php'], []); } @@ -192,7 +168,6 @@ public function testBug7424(): void public function testBug7724(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/bug-7724.php'], []); } @@ -200,7 +175,6 @@ public function testBug7724(): void public function testBug7199(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/bug-7199.php'], []); } @@ -208,26 +182,46 @@ public function testBug7199(): void public function testBug9126(): void { $this->treatPhpDocTypesAsCertain = false; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/bug-9126.php'], []); } - public function dataBug9403(): iterable + public static function dataBug9403(): iterable { yield [true]; yield [false]; } - /** - * @dataProvider dataBug9403 - */ + #[DataProvider('dataBug9403')] public function testBug9403(bool $treatPhpDocTypesAsCertain): void { $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/bug-9403.php'], []); } + public function testBug12658(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-12658.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 2c22dfd35a..8ea1fb0443 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -7,7 +7,7 @@ use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -17,8 +17,6 @@ class IssetRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; - private bool $strictUnnecessaryNullsafePropertyFetch; - protected function getRule(): Rule { return new IssetRule(new IssetCheck( @@ -26,7 +24,6 @@ protected function getRule(): Rule new PropertyReflectionFinder(), true, $this->treatPhpDocTypesAsCertain, - $this->strictUnnecessaryNullsafePropertyFetch, )); } @@ -35,10 +32,14 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; + } + public function testRule(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/isset.php'], [ [ 'Property IssetRule\FooCoalesce::$string (string) in isset() is not nullable.', @@ -126,7 +127,6 @@ public function testRule(): void public function testRuleWithoutTreatPhpDocTypesAsCertain(): void { $this->treatPhpDocTypesAsCertain = false; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/isset.php'], [ [ 'Property IssetRule\FooCoalesce::$string (string) in isset() is not nullable.', @@ -206,7 +206,6 @@ public function testRuleWithoutTreatPhpDocTypesAsCertain(): void public function testNativePropertyTypes(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/isset-native-property-types.php'], [ /*[ // no way to achieve this with current PHP Reflection API @@ -225,24 +224,18 @@ public function testNativePropertyTypes(): void public function testBug4290(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/bug-4290.php'], []); } public function testBug4671(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; - $this->analyse([__DIR__ . '/data/bug-4671.php'], [[ - 'Offset numeric-string on array in isset() does not exist.', - 13, - ]]); + $this->analyse([__DIR__ . '/data/bug-4671.php'], []); } public function testVariableCertaintyInIsset(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/variable-certainty-isset.php'], [ [ 'Variable $alwaysDefinedNotNullable in isset() always exists and is not nullable.', @@ -277,10 +270,12 @@ 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, ], [ + // could be Variable $variableInSecondCase in isset() always exists and is not nullable. 'Variable $variableInSecondCase in isset() is never defined.', 117, ], @@ -318,7 +313,6 @@ public function testVariableCertaintyInIsset(): void public function testIssetInGlobalScope(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/isset-global-scope.php'], [ [ 'Variable $alwaysDefinedNotNullable in isset() always exists and is not nullable.', @@ -330,35 +324,18 @@ public function testIssetInGlobalScope(): void public function testNullsafe(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; - $this->analyse([__DIR__ . '/data/isset-nullsafe.php'], []); - } - - public function testBug7109(): void - { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - - $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; - - $this->analyse([__DIR__ . '/../Properties/data/bug-7109.php'], [ + $this->analyse([__DIR__ . '/data/isset-nullsafe.php'], [ [ - 'Expression in isset() is not nullable.', - 41, + 'Using nullsafe property access "?->bla" in isset() is unnecessary. Use -> instead.', + 10, ], ]); } - public function testBug7109Strict(): void + #[RequiresPhp('>= 8.0')] + public function testBug7109(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = true; $this->analyse([__DIR__ . '/../Properties/data/bug-7109.php'], [ [ @@ -378,7 +355,7 @@ public function testBug7109Strict(): void 67, ], [ - 'Using nullsafe property access "?->(Expression)" in isset() is unnecessary. Use -> instead.', + 'Expression in isset() is not nullable.', 74, ], ]); @@ -387,7 +364,6 @@ public function testBug7109Strict(): void public function testBug7318(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = true; $this->analyse([__DIR__ . '/../Properties/data/bug-7318.php'], [ [ @@ -400,7 +376,6 @@ public function testBug7318(): void public function testBug6163(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = true; $this->analyse([__DIR__ . '/data/bug-6163.php'], []); } @@ -408,19 +383,14 @@ public function testBug6163(): void public function testBug6997(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = true; $this->analyse([__DIR__ . '/data/bug-6997.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug7776(): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-7776.php'], []); } @@ -428,7 +398,6 @@ public function testBug7776(): void public function testBug6008(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = true; $this->analyse([__DIR__ . '/data/bug-6008.php'], []); } @@ -436,7 +405,6 @@ public function testBug6008(): void public function testBug7292(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = true; $this->analyse([__DIR__ . '/data/bug-7292.php'], []); } @@ -444,7 +412,6 @@ public function testBug7292(): void public function testObjectShapes(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = true; // could be checked but current is not $this->analyse([__DIR__ . '/data/isset-object-shapes.php'], []); @@ -453,7 +420,6 @@ public function testObjectShapes(): void public function testBug10151(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = true; $this->analyse([__DIR__ . '/data/bug-10151.php'], []); } @@ -461,7 +427,6 @@ public function testBug10151(): void public function testBug3985(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3985.php'], [ [ @@ -478,9 +443,53 @@ public function testBug3985(): void public function testBug10064(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = 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, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php index 4781fa1f18..3c18294d1c 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -7,6 +7,7 @@ use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use const PHP_VERSION_ID; /** @@ -17,8 +18,6 @@ class NullCoalesceRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; - private bool $strictUnnecessaryNullsafePropertyFetch; - protected function getRule(): Rule { return new NullCoalesceRule(new IssetCheck( @@ -26,7 +25,6 @@ protected function getRule(): Rule new PropertyReflectionFinder(), true, $this->treatPhpDocTypesAsCertain, - $this->strictUnnecessaryNullsafePropertyFetch, )); } @@ -35,10 +33,14 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; + } + public function testCoalesceRule(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $errors = [ [ 'Property CoalesceRule\FooCoalesce::$string (string) on left side of ?? is not nullable.', @@ -145,7 +147,6 @@ public function testCoalesceRule(): void public function testCoalesceAssignRule(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/null-coalesce-assign.php'], [ [ 'Property CoalesceAssignRule\FooCoalesce::$string (string) on left side of ??= is not nullable.', @@ -209,14 +210,12 @@ public function testCoalesceAssignRule(): void public function testNullsafe(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/null-coalesce-nullsafe.php'], []); } public function testVariableCertaintyInNullCoalesce(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/variable-certainty-null.php'], [ [ 'Variable $scalar on left side of ?? always exists and is not nullable.', @@ -236,7 +235,6 @@ public function testVariableCertaintyInNullCoalesce(): void public function testVariableCertaintyInNullCoalesceAssign(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/variable-certainty-null-assign.php'], [ [ 'Variable $scalar on left side of ??= always exists and is not nullable.', @@ -256,7 +254,6 @@ public function testVariableCertaintyInNullCoalesceAssign(): void public function testNullCoalesceInGlobalScope(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/null-coalesce-global-scope.php'], [ [ 'Variable $bar on left side of ?? always exists and is not nullable.', @@ -268,35 +265,13 @@ public function testNullCoalesceInGlobalScope(): void public function testBug5933(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/bug-5933.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug7109(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - - $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; - - $this->analyse([__DIR__ . '/../Properties/data/bug-7109.php'], [ - [ - 'Expression on left side of ?? is not nullable.', - 40, - ], - ]); - } - - public function testBug7109Strict(): void - { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = true; $this->analyse([__DIR__ . '/../Properties/data/bug-7109.php'], [ [ @@ -316,7 +291,7 @@ public function testBug7109Strict(): void 66, ], [ - 'Using nullsafe property access "?->(Expression)" on left side of ?? is unnecessary. Use -> instead.', + 'Expression on left side of ?? is not nullable.', 73, ], ]); @@ -324,12 +299,7 @@ public function testBug7109Strict(): void public function testBug7190(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/../Properties/data/bug-7190.php'], [ [ @@ -342,7 +312,6 @@ public function testBug7190(): void public function testBug7318(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = true; $this->analyse([__DIR__ . '/../Properties/data/bug-7318.php'], [ [ @@ -355,7 +324,6 @@ public function testBug7318(): void public function testBug7968(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = true; $this->analyse([__DIR__ . '/data/bug-7968.php'], []); } @@ -363,7 +331,6 @@ public function testBug7968(): void public function testBug8084(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = true; $this->analyse([__DIR__ . '/data/bug-8084.php'], []); } @@ -371,17 +338,41 @@ public function testBug8084(): void public function testBug10577(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = true; $this->analyse([__DIR__ . '/data/bug-10577.php'], []); } + public function testBug11708(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-11708.php'], []); + } + public function testBug10610(): void { $this->treatPhpDocTypesAsCertain = true; - $this->strictUnnecessaryNullsafePropertyFetch = true; $this->analyse([__DIR__ . '/data/bug-10610.php'], []); } + #[RequiresPhp('>= 8.4')] + public function testBug12553(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12553.php'], []); + } + + public function testIssetAfterRememberedConstructor(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/isset-after-remembered-constructor.php'], [ + [ + 'Property IssetOrCoalesceOnNonNullableInitializedProperty\User::$string on left side of ?? is not nullable nor uninitialized.', + 46, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php index ed68b380b1..bdd78d2dfd 100644 --- a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php +++ b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule as TRule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -15,7 +16,7 @@ class ParameterOutAssignedTypeRuleTest extends RuleTestCase protected function getRule(): TRule { return new ParameterOutAssignedTypeRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, true, false, true, false), + new RuleLevelHelper(self::createReflectionProvider(), true, false, true, true, false, false, true), ); } @@ -43,7 +44,7 @@ public function testRule(): void 47, ], [ - 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doBaz3() expects list>, array, array, int>> given.', + 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doBaz3() expects list>, list, int>> given.', 56, ], [ @@ -64,4 +65,20 @@ 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 index 26157f4ffb..c2af7472ea 100644 --- a/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php +++ b/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php @@ -15,7 +15,7 @@ class ParameterOutExecutionEndTypeRuleTest extends RuleTestCase protected function getRule(): Rule { return new ParameterOutExecutionEndTypeRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, true, false, true, false), + new RuleLevelHelper(self::createReflectionProvider(), true, false, true, true, false, false, true), ); } @@ -58,4 +58,9 @@ 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 020f713708..0000000000 --- a/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php +++ /dev/null @@ -1,70 +0,0 @@ - - */ -class ThrowTypeRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new ThrowTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false)); - } - - public function testRule(): void - { - $this->analyse( - [__DIR__ . '/data/throw-values.php'], - [ - [ - 'Invalid type int to throw.', - 29, - ], - [ - 'Invalid type ThrowValues\InvalidException to throw.', - 32, - ], - [ - 'Invalid type ThrowValues\InvalidInterfaceException to throw.', - 35, - ], - [ - 'Invalid type Exception|null to throw.', - 38, - ], - [ - 'Throwing object of an unknown class ThrowValues\NonexistentClass.', - 44, - 'Learn more at https://phpstan.org/user-guide/discovering-symbols', - ], - ], - ); - } - - public function testClassExists(): void - { - $this->analyse([__DIR__ . '/data/throw-class-exists.php'], []); - } - - public function testRuleWithNullsafeVariant(): void - { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - - $this->analyse([__DIR__ . '/data/throw-values-nullsafe.php'], [ - [ - 'Invalid type Exception|null to throw.', - 17, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Variables/UnsetRuleTest.php b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php index 036d09c974..c9cb149958 100644 --- a/tests/PHPStan/Rules/Variables/UnsetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php @@ -2,8 +2,13 @@ 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 RuleTestCase @@ -13,7 +18,10 @@ class UnsetRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UnsetRule(); + return new UnsetRule( + self::getContainer()->getByType(PropertyReflectionFinder::class), + self::getContainer()->getByType(PhpVersion::class), + ); } public function testUnsetRule(): void @@ -91,4 +99,123 @@ 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 801d2b31f4..b9e7a170f0 100644 --- a/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php +++ b/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php @@ -5,7 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -15,7 +15,7 @@ class VariableCloningRuleTest extends RuleTestCase protected function getRule(): Rule { - return new VariableCloningRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false)); + return new VariableCloningRule(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true)); } public function testClone(): void @@ -49,12 +49,9 @@ public function testClone(): void ]); } + #[RequiresPhp('>= 8.0')] public function testRuleWithNullsafeVariant(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/variable-cloning-nullsafe.php'], [ [ 'Cannot clone stdClass|null.', diff --git a/tests/PHPStan/Rules/Variables/data/bug-10151.php b/tests/PHPStan/Rules/Variables/data/bug-10151.php index f93e860eb8..78d5c4219d 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-10151.php +++ b/tests/PHPStan/Rules/Variables/data/bug-10151.php @@ -1,4 +1,4 @@ -= 7.4 +>} $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 index bfe756a020..2d57843622 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-3391.php +++ b/tests/PHPStan/Rules/Variables/data/bug-3391.php @@ -22,11 +22,11 @@ public function test() $data['foo'] = 'a'; $data['bar'] = 'b'; - assertType("hasOffsetValue('bar', 'b')&hasOffsetValue('foo', 'a')&non-empty-array", $data); + assertType("non-empty-array&hasOffsetValue('bar', 'b')&hasOffsetValue('foo', 'a')", $data); unset($data['id']); - assertType("array&hasOffsetValue('bar', 'b')&hasOffsetValue('foo', 'a')", $data); + assertType("non-empty-array&hasOffsetValue('bar', 'b')&hasOffsetValue('foo', 'a')", $data); return $data; } } diff --git a/tests/PHPStan/Rules/Variables/data/bug-7292.php b/tests/PHPStan/Rules/Variables/data/bug-7292.php index d4d09b2ed4..9e66bb9793 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-7292.php +++ b/tests/PHPStan/Rules/Variables/data/bug-7292.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 +', $review); + assertType('array>', $review); if ( array_key_exists('review', $review['SurveyInvitation']) && $review['SurveyInvitation']['review'] === null ) { - assertType("array&hasOffsetValue('SurveyInvitation', array&hasOffsetValue('review', null))", $review); + 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', array&hasOffsetValue('review', null))", $review); + assertType("non-empty-array>&hasOffsetValue('Review', array{id: null, text: null, answer: null})&hasOffsetValue('SurveyInvitation', non-empty-array&hasOffsetValue('review', null))", $review); unset($review['SurveyInvitation']['review']); - assertType("non-empty-array&hasOffsetValue('Review', array)&hasOffsetValue('SurveyInvitation', array)", $review); + assertType("non-empty-array>&hasOffsetValue('Review', array{id: null, text: null, answer: null})&hasOffsetValue('SurveyInvitation', array)", $review); } - assertType('array', $review); + assertType('array>', $review); if (array_key_exists('User', $review['Review'])) { - assertType("array&hasOffsetValue('Review', array&hasOffset('User'))", $review); + assertType("non-empty-array>&hasOffsetValue('Review', non-empty-array&hasOffset('User'))", $review); $review['User'] = $review['Review']['User']; - assertType("hasOffsetValue('Review', array&hasOffset('User'))&hasOffsetValue('User', mixed)&non-empty-array", $review); + assertType("non-empty-array&hasOffsetValue('Review', non-empty-array&hasOffset('User'))&hasOffsetValue('User', mixed)", $review); unset($review['Review']['User']); - assertType("hasOffsetValue('Review', array)&hasOffsetValue('User', array)&non-empty-array", $review); + assertType("non-empty-array&hasOffsetValue('Review', array)&hasOffsetValue('User', mixed)", $review); } - assertType("array&hasOffsetValue('Review', array)", $review); + assertType("non-empty-array&hasOffsetValue('Review', array)", $review); }; diff --git a/tests/PHPStan/Rules/Variables/data/bug-8719.php b/tests/PHPStan/Rules/Variables/data/bug-8719.php new file mode 100644 index 0000000000..d1d3337111 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-8719.php @@ -0,0 +1,50 @@ +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-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/defined-variables-anonymous-function-use.php b/tests/PHPStan/Rules/Variables/data/defined-variables-anonymous-function-use.php index eb2599f89e..e02deb647a 100644 --- a/tests/PHPStan/Rules/Variables/data/defined-variables-anonymous-function-use.php +++ b/tests/PHPStan/Rules/Variables/data/defined-variables-anonymous-function-use.php @@ -14,7 +14,7 @@ function () use (&$errorHandler) { $onlyInIf = 1; } -for ($forI = 0; $forI < 10, $anotherVariableFromForCond = 1; $forI++, $forJ = $forI) { +for ($forI = 0; $anotherVariableFromForCond = 1, $forI < 10; $forI++, $forJ = $forI) { } diff --git a/tests/PHPStan/Rules/Variables/data/defined-variables-arrow-functions.php b/tests/PHPStan/Rules/Variables/data/defined-variables-arrow-functions.php index 10b0fdcb42..3196608c7a 100644 --- a/tests/PHPStan/Rules/Variables/data/defined-variables-arrow-functions.php +++ b/tests/PHPStan/Rules/Variables/data/defined-variables-arrow-functions.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 +name}; + } + + public function testScope(): void + { + $name1 = 'foo'; + $rand = rand(); + if ($rand === 1) { + $foo = 1; + $name = $name1; + } elseif ($rand === 2) { + $name = 'bar'; + $bar = 'str'; + } else { + $name = 'buz'; + } + + if ($name === 'foo') { + echo $$name; // ok + echo $foo; // ok + echo $bar; + } elseif ($name === 'bar') { + echo $$name; // ok + echo $foo; + echo $bar; // ok + } else { + echo $$name; // ok + echo $foo; + echo $bar; + } + + echo $$name; // ok + echo $foo; + echo $bar; + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/isset-after-remembered-constructor.php b/tests/PHPStan/Rules/Variables/data/isset-after-remembered-constructor.php new file mode 100644 index 0000000000..2c48e6249d --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/isset-after-remembered-constructor.php @@ -0,0 +1,98 @@ += 8.2 + +namespace IssetOrCoalesceOnNonNullableInitializedProperty; + +class User +{ + private ?string $nullableString; + private string $maybeUninitializedString; + private string $string; + + private $untyped; + + public function __construct() + { + if (rand(0, 1)) { + $this->nullableString = 'hello'; + $this->string = 'world'; + $this->maybeUninitializedString = 'something'; + } else { + $this->nullableString = null; + $this->string = 'world 2'; + $this->untyped = 123; + } + } + + public function doFoo(): void + { + if (isset($this->maybeUninitializedString)) { + echo $this->maybeUninitializedString; + } + if (isset($this->nullableString)) { + echo $this->nullableString; + } + if (isset($this->string)) { + echo $this->string; + } + if (isset($this->untyped)) { + echo $this->untyped; + } + } + + public function doBar(): void + { + echo $this->maybeUninitializedString ?? 'default'; + echo $this->nullableString ?? 'default'; + echo $this->string ?? 'default'; + echo $this->untyped ?? 'default'; + } + + public function doFooBar(): void + { + if (empty($this->maybeUninitializedString)) { + echo $this->maybeUninitializedString; + } + if (empty($this->nullableString)) { + echo $this->nullableString; + } + if (empty($this->string)) { + echo $this->string; + } + if (empty($this->untyped)) { + echo $this->untyped; + } + } +} + +class MoreEmptyCases +{ + private false|string $union; + private false $false; + private true $true; + private bool $bool; + + public function __construct() + { + if (rand(0, 1)) { + $this->union = 'nope'; + $this->bool = true; + } elseif (rand(10, 20)) { + $this->union = false; + $this->bool = false; + } + $this->false = false; + $this->true = true; + } + + public function doFoo(): void + { + if (empty($this->union)) { + } + if (empty($this->bool)) { + } + if (empty($this->false)) { + } + if (empty($this->true)) { + } + } +} diff --git a/tests/PHPStan/Rules/Variables/data/isset-native-property-types.php b/tests/PHPStan/Rules/Variables/data/isset-native-property-types.php index f493c1fac6..1d93d3f647 100644 --- a/tests/PHPStan/Rules/Variables/data/isset-native-property-types.php +++ b/tests/PHPStan/Rules/Variables/data/isset-native-property-types.php @@ -1,4 +1,4 @@ -= 7.4 += 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/null-coalesce-assign.php b/tests/PHPStan/Rules/Variables/data/null-coalesce-assign.php index df69c39680..5f1377113b 100644 --- a/tests/PHPStan/Rules/Variables/data/null-coalesce-assign.php +++ b/tests/PHPStan/Rules/Variables/data/null-coalesce-assign.php @@ -1,4 +1,4 @@ -= 7.4 += 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/throw-class-exists.php b/tests/PHPStan/Rules/Variables/data/throw-class-exists.php deleted file mode 100644 index f819307398..0000000000 --- a/tests/PHPStan/Rules/Variables/data/throw-class-exists.php +++ /dev/null @@ -1,19 +0,0 @@ -= 8.0 - -namespace ThrowValuesNullsafe; - -class Bar -{ - - function doException(): \Exception - { - return new \Exception(); - } - -} - -function doFoo(?Bar $bar) -{ - throw $bar?->doException(); -} diff --git a/tests/PHPStan/Rules/Variables/data/throw-values.php b/tests/PHPStan/Rules/Variables/data/throw-values.php deleted file mode 100644 index 5582923fa2..0000000000 --- a/tests/PHPStan/Rules/Variables/data/throw-values.php +++ /dev/null @@ -1,62 +0,0 @@ - $genericExceptionClassName - * @param T $genericException - */ -function test($genericExceptionClassName, $genericException) { - /** @var ValidInterfaceException $validInterface */ - $validInterface = new \Exception(); - /** @var InvalidInterfaceException $invalidInterface */ - $invalidInterface = new \Exception(); - /** @var \Exception|null $nullableException */ - $nullableException = new \Exception(); - - if (rand(0, 1)) { - throw new \Exception(); - } - if (rand(0, 1)) { - throw $validInterface; - } - if (rand(0, 1)) { - throw 123; - } - if (rand(0, 1)) { - throw new InvalidException(); - } - if (rand(0, 1)) { - throw $invalidInterface; - } - if (rand(0, 1)) { - throw $nullableException; - } - if (rand(0, 1)) { - throw foo(); - } - if (rand(0, 1)) { - throw new NonexistentClass(); - } - if (rand(0, 1)) { - throw new $genericExceptionClassName; - } - if (rand(0, 1)) { - throw $genericException; - } -} - -function (\stdClass $foo) { - /** @var \Exception $foo */ - throw $foo; -}; - -function (\stdClass $foo) { - /** @var \Exception */ - throw $foo; -}; 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-null-assign.php b/tests/PHPStan/Rules/Variables/data/variable-certainty-null-assign.php index 8500b14063..d0d2b7c927 100644 --- a/tests/PHPStan/Rules/Variables/data/variable-certainty-null-assign.php +++ b/tests/PHPStan/Rules/Variables/data/variable-certainty-null-assign.php @@ -1,4 +1,4 @@ -= 7.4 + - */ -class WarningEmittingRuleTest extends RuleTestCase -{ - - /** - * @return Rule - */ - protected function getRule(): Rule - { - return new class implements Rule { - - public function getNodeType(): string - { - return Node::class; - } - - public function processNode(Node $node, Scope $scope): array - { - echo $undefined; // @phpstan-ignore variable.undefined - return []; - } - - }; - } - - public function testRule(): void - { - if (PHP_VERSION_ID < 70300) { - self::markTestSkipped('For some reason this test does not work on PHP 7.2 with old PHPUnit'); - } - - try { - $this->analyse([__DIR__ . '/data/empty-file.php'], []); - self::fail('Should throw an exception'); - - } catch (AssertionFailedError $e) { - self::assertStringContainsString('Undefined variable', $e->getMessage()); // exact message differs between PHPStan versions - } - } - -} diff --git a/tests/PHPStan/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 index eae3f7a3e2..054094eebb 100644 --- a/tests/PHPStan/Testing/TypeInferenceTestCaseTest.php +++ b/tests/PHPStan/Testing/TypeInferenceTestCaseTest.php @@ -4,6 +4,8 @@ use PHPStan\File\FileHelper; use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\Attributes\DataProvider; +use function array_values; use function sprintf; final class TypeInferenceTestCaseTest extends TypeInferenceTestCase @@ -35,6 +37,13 @@ public static function dataFileAssertionFailedErrors(): iterable $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( @@ -56,6 +65,13 @@ public static function dataFileAssertionFailedErrors(): iterable $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( @@ -77,17 +93,74 @@ public static function dataFileAssertionFailedErrors(): iterable $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 - */ + #[DataProvider('dataFileAssertionFailedErrors')] public function testFileAssertionFailedErrors(string $filePath, string $errorMessage): void { $this->expectException(AssertionFailedError::class); $this->expectExceptionMessage($errorMessage); - $this->gatherAssertTypes($filePath); + 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-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 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 @@ +assertTrue($expectedResult->equals($value->and(...$operands))); } - /** - * @dataProvider dataAnd - */ + #[DataProvider('dataAnd')] public function testLazyAnd( TrinaryLogic $expectedResult, TrinaryLogic $value, @@ -52,7 +49,7 @@ public function testLazyAnd( $this->assertTrue($expectedResult->equals($value->lazyAnd($operands, static fn (TrinaryLogic $result) => $result))); } - public function dataOr(): array + public static function dataOr(): array { return [ [TrinaryLogic::createNo(), TrinaryLogic::createNo()], @@ -73,9 +70,7 @@ public function dataOr(): array ]; } - /** - * @dataProvider dataOr - */ + #[DataProvider('dataOr')] public function testOr( TrinaryLogic $expectedResult, TrinaryLogic $value, @@ -85,9 +80,7 @@ public function testOr( $this->assertTrue($expectedResult->equals($value->or(...$operands))); } - /** - * @dataProvider dataOr - */ + #[DataProvider('dataOr')] public function testLazyOr( TrinaryLogic $expectedResult, TrinaryLogic $value, @@ -97,7 +90,7 @@ public function testLazyOr( $this->assertTrue($expectedResult->equals($value->lazyOr($operands, static fn (TrinaryLogic $result) => $result))); } - public function dataNegate(): array + public static function dataNegate(): array { return [ [TrinaryLogic::createNo(), TrinaryLogic::createYes()], @@ -106,15 +99,13 @@ public function dataNegate(): array ]; } - /** - * @dataProvider dataNegate - */ + #[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(); @@ -153,9 +144,7 @@ public function dataCompareTo(): array ]; } - /** - * @dataProvider dataCompareTo - */ + #[DataProvider('dataCompareTo')] public function testCompareTo(TrinaryLogic $first, TrinaryLogic $second, ?TrinaryLogic $expected): void { $this->assertSame( @@ -164,9 +153,7 @@ public function testCompareTo(TrinaryLogic $first, TrinaryLogic $second, ?Trinar ); } - /** - * @dataProvider dataCompareTo - */ + #[DataProvider('dataCompareTo')] public function testCompareToInversed(TrinaryLogic $first, TrinaryLogic $second, ?TrinaryLogic $expected): void { $this->assertSame( diff --git a/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php b/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php index a70ff82ceb..057ae63a2f 100644 --- a/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php +++ b/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php @@ -17,12 +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 PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ [ @@ -70,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()), @@ -141,9 +137,7 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(HasMethodType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -154,7 +148,7 @@ public function testIsSuperTypeOf(HasMethodType $type, Type $otherType, TrinaryL ); } - public function dataIsSubTypeOf(): array + public static function dataIsSubTypeOf(): array { return [ [ @@ -191,9 +185,7 @@ public function dataIsSubTypeOf(): array ]; } - /** - * @dataProvider dataIsSubTypeOf - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOf(HasMethodType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSubTypeOf($otherType); @@ -204,9 +196,7 @@ public function testIsSubTypeOf(HasMethodType $type, Type $otherType, TrinaryLog ); } - /** - * @dataProvider dataIsSubTypeOf - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOfInversed(HasMethodType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $otherType->isSuperTypeOf($type); diff --git a/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php b/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php index 52b44a6168..087bd96910 100644 --- a/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php +++ b/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php @@ -16,13 +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 PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ [ @@ -60,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()), @@ -107,9 +103,7 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(HasPropertyType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -120,7 +114,7 @@ public function testIsSuperTypeOf(HasPropertyType $type, Type $otherType, Trinar ); } - public function dataIsSubTypeOf(): array + public static function dataIsSubTypeOf(): array { return [ [ @@ -152,9 +146,7 @@ public function dataIsSubTypeOf(): array ]; } - /** - * @dataProvider dataIsSubTypeOf - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOf(HasPropertyType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSubTypeOf($otherType); @@ -165,9 +157,7 @@ public function testIsSubTypeOf(HasPropertyType $type, Type $otherType, TrinaryL ); } - /** - * @dataProvider dataIsSubTypeOf - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOfInversed(HasPropertyType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $otherType->isSuperTypeOf($type); diff --git a/tests/PHPStan/Type/ArrayTypeTest.php b/tests/PHPStan/Type/ArrayTypeTest.php index cd8ddce5bf..9b7b2cfcaf 100644 --- a/tests/PHPStan/Type/ArrayTypeTest.php +++ b/tests/PHPStan/Type/ArrayTypeTest.php @@ -12,13 +12,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 ArrayTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ [ @@ -57,12 +58,12 @@ 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(), ], @@ -71,12 +72,21 @@ public function dataIsSuperTypeOf(): array 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 - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(ArrayType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -87,7 +97,7 @@ public function testIsSuperTypeOf(ArrayType $type, Type $otherType, TrinaryLogic ); } - public function dataAccepts(): array + public static function dataAccepts(): array { $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); @@ -136,16 +146,14 @@ public function dataAccepts(): array ]; } - /** - * @dataProvider dataAccepts - */ + #[DataProvider('dataAccepts')] public function testAccepts( ArrayType $acceptingType, Type $acceptedType, TrinaryLogic $expectedResult, ): void { - $actualResult = $acceptingType->accepts($acceptedType, true); + $actualResult = $acceptingType->accepts($acceptedType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), @@ -153,7 +161,7 @@ public function testAccepts( ); } - public function dataDescribe(): array + public static function dataDescribe(): array { return [ [ @@ -166,9 +174,7 @@ public function dataDescribe(): array ]; } - /** - * @dataProvider dataDescribe - */ + #[DataProvider('dataDescribe')] public function testDescribe( ArrayType $type, string $expectedDescription, @@ -177,7 +183,7 @@ public function testDescribe( $this->assertSame($expectedDescription, $type->describe(VerbosityLevel::precise())); } - public function dataInferTemplateTypes(): array + public static function dataInferTemplateTypes(): array { $templateType = static fn ($name): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), @@ -250,9 +256,9 @@ 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); diff --git a/tests/PHPStan/Type/BenevolentUnionTypeTest.php b/tests/PHPStan/Type/BenevolentUnionTypeTest.php index 18a37f8be0..0610f74c4f 100644 --- a/tests/PHPStan/Type/BenevolentUnionTypeTest.php +++ b/tests/PHPStan/Type/BenevolentUnionTypeTest.php @@ -14,12 +14,13 @@ use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantStringType; +use PHPUnit\Framework\Attributes\DataProvider; use function sprintf; class BenevolentUnionTypeTest extends PHPStanTestCase { - public function dataCanAccessProperties(): Iterator + public static function dataCanAccessProperties(): Iterator { yield [ new BenevolentUnionType([new ObjectWithoutClassType(), new ObjectWithoutClassType()]), @@ -37,7 +38,7 @@ public function dataCanAccessProperties(): Iterator ]; } - /** @dataProvider dataCanAccessProperties */ + #[DataProvider('dataCanAccessProperties')] public function testCanAccessProperties(BenevolentUnionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->canAccessProperties(); @@ -48,7 +49,7 @@ public function testCanAccessProperties(BenevolentUnionType $type, TrinaryLogic ); } - public function dataHasProperty(): Iterator + public static function dataHasProperty(): Iterator { yield [ new BenevolentUnionType([ @@ -75,7 +76,7 @@ public function dataHasProperty(): Iterator ]; } - /** @dataProvider dataHasProperty */ + #[DataProvider('dataHasProperty')] public function testHasProperty(BenevolentUnionType $type, string $propertyName, TrinaryLogic $expectedResult): void { $actualResult = $type->hasProperty($propertyName); @@ -86,7 +87,7 @@ public function testHasProperty(BenevolentUnionType $type, string $propertyName, ); } - public function dataCanCallMethods(): Iterator + public static function dataCanCallMethods(): Iterator { yield [ new BenevolentUnionType([new ObjectWithoutClassType(), new ObjectWithoutClassType()]), @@ -104,7 +105,7 @@ public function dataCanCallMethods(): Iterator ]; } - /** @dataProvider dataCanCallMethods */ + #[DataProvider('dataCanCallMethods')] public function testCanCanCallMethods(BenevolentUnionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->canCallMethods(); @@ -115,7 +116,7 @@ public function testCanCanCallMethods(BenevolentUnionType $type, TrinaryLogic $e ); } - public function dataHasMethod(): Iterator + public static function dataHasMethod(): Iterator { yield [ new BenevolentUnionType([ @@ -139,7 +140,7 @@ public function dataHasMethod(): Iterator ]; } - /** @dataProvider dataHasMethod */ + #[DataProvider('dataHasMethod')] public function testHasMethod(BenevolentUnionType $type, string $methodName, TrinaryLogic $expectedResult): void { $actualResult = $type->hasMethod($methodName); @@ -150,7 +151,7 @@ public function testHasMethod(BenevolentUnionType $type, string $methodName, Tri ); } - public function dataCanAccessConstants(): Iterator + public static function dataCanAccessConstants(): Iterator { yield [ new BenevolentUnionType([new ObjectWithoutClassType(), new ObjectWithoutClassType()]), @@ -168,7 +169,7 @@ public function dataCanAccessConstants(): Iterator ]; } - /** @dataProvider dataCanAccessConstants */ + #[DataProvider('dataCanAccessConstants')] public function testCanAccessConstants(BenevolentUnionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->canAccessConstants(); @@ -179,7 +180,7 @@ public function testCanAccessConstants(BenevolentUnionType $type, TrinaryLogic $ ); } - public function dataIsIterable(): Iterator + public static function dataIsIterable(): Iterator { yield [ new BenevolentUnionType([ @@ -203,7 +204,7 @@ public function dataIsIterable(): Iterator ]; } - /** @dataProvider dataIsIterable */ + #[DataProvider('dataIsIterable')] public function testIsIterable(BenevolentUnionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isIterable(); @@ -214,7 +215,7 @@ public function testIsIterable(BenevolentUnionType $type, TrinaryLogic $expected ); } - public function dataIsIterableAtLeastOnce(): Iterator + public static function dataIsIterableAtLeastOnce(): Iterator { yield [ new BenevolentUnionType([ @@ -238,7 +239,7 @@ public function dataIsIterableAtLeastOnce(): Iterator ]; } - /** @dataProvider dataIsIterableAtLeastOnce */ + #[DataProvider('dataIsIterableAtLeastOnce')] public function testIsIterableAtLeastOnce(BenevolentUnionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isIterableAtLeastOnce(); @@ -249,7 +250,7 @@ public function testIsIterableAtLeastOnce(BenevolentUnionType $type, TrinaryLogi ); } - public function dataIsArray(): Iterator + public static function dataIsArray(): Iterator { yield [ new BenevolentUnionType([new ArrayType(new MixedType(), new MixedType()), new ConstantArrayType([], [])]), @@ -267,7 +268,7 @@ public function dataIsArray(): Iterator ]; } - /** @dataProvider dataIsArray */ + #[DataProvider('dataIsArray')] public function testIsArray(BenevolentUnionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isArray(); @@ -278,7 +279,7 @@ public function testIsArray(BenevolentUnionType $type, TrinaryLogic $expectedRes ); } - public function dataIsString(): Iterator + public static function dataIsString(): Iterator { yield [ new BenevolentUnionType([ @@ -299,7 +300,7 @@ public function dataIsString(): Iterator ]; } - /** @dataProvider dataIsString */ + #[DataProvider('dataIsString')] public function testIsString(BenevolentUnionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isString(); @@ -310,7 +311,7 @@ public function testIsString(BenevolentUnionType $type, TrinaryLogic $expectedRe ); } - public function dataIsNumericString(): Iterator + public static function dataIsNumericString(): Iterator { yield [ new BenevolentUnionType([ @@ -330,7 +331,7 @@ public function dataIsNumericString(): Iterator ]; } - /** @dataProvider dataIsNumericString */ + #[DataProvider('dataIsNumericString')] public function testIsNumericString(BenevolentUnionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isNumericString(); @@ -341,7 +342,7 @@ public function testIsNumericString(BenevolentUnionType $type, TrinaryLogic $exp ); } - public function dataIsNonFalsyString(): Iterator + public static function dataIsNonFalsyString(): Iterator { yield [ new BenevolentUnionType([ @@ -361,7 +362,7 @@ public function dataIsNonFalsyString(): Iterator ]; } - /** @dataProvider dataIsNonFalsyString */ + #[DataProvider('dataIsNonFalsyString')] public function testIsNonFalsyString(BenevolentUnionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isNonFalsyString(); @@ -372,7 +373,7 @@ public function testIsNonFalsyString(BenevolentUnionType $type, TrinaryLogic $ex ); } - public function dataIsLiteralString(): Iterator + public static function dataIsLiteralString(): Iterator { yield [ new BenevolentUnionType([ @@ -392,7 +393,7 @@ public function dataIsLiteralString(): Iterator ]; } - /** @dataProvider dataIsLiteralString */ + #[DataProvider('dataIsLiteralString')] public function testIsLiteralString(BenevolentUnionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isLiteralString(); @@ -403,7 +404,7 @@ public function testIsLiteralString(BenevolentUnionType $type, TrinaryLogic $exp ); } - public function dataIsOffsetAccesible(): Iterator + public static function dataIsOffsetAccesible(): Iterator { yield [ new BenevolentUnionType([ @@ -427,7 +428,7 @@ public function dataIsOffsetAccesible(): Iterator ]; } - /** @dataProvider dataIsOffsetAccesible */ + #[DataProvider('dataIsOffsetAccesible')] public function testIsOffsetAccessible(BenevolentUnionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isOffsetAccessible(); @@ -438,7 +439,7 @@ public function testIsOffsetAccessible(BenevolentUnionType $type, TrinaryLogic $ ); } - public function dataHasOffsetValueType(): Iterator + public static function dataHasOffsetValueType(): Iterator { yield [ new BenevolentUnionType([ @@ -465,7 +466,7 @@ public function dataHasOffsetValueType(): Iterator ]; } - /** @dataProvider dataHasOffsetValueType */ + #[DataProvider('dataHasOffsetValueType')] public function testHasOffsetValue(BenevolentUnionType $type, Type $offsetType, TrinaryLogic $expectedResult): void { $actualResult = $type->hasOffsetValueType($offsetType); @@ -476,7 +477,7 @@ public function testHasOffsetValue(BenevolentUnionType $type, Type $offsetType, ); } - public function dataIsCallable(): Iterator + public static function dataIsCallable(): Iterator { yield [ new BenevolentUnionType([new CallableType(), new CallableType()]), @@ -494,7 +495,7 @@ public function dataIsCallable(): Iterator ]; } - /** @dataProvider dataIsCallable */ + #[DataProvider('dataIsCallable')] public function testIsCallable(BenevolentUnionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isCallable(); @@ -505,7 +506,7 @@ public function testIsCallable(BenevolentUnionType $type, TrinaryLogic $expected ); } - public function dataIsCloneable(): Iterator + public static function dataIsCloneable(): Iterator { yield [ new BenevolentUnionType([new ObjectWithoutClassType(), new ObjectWithoutClassType()]), @@ -523,7 +524,7 @@ public function dataIsCloneable(): Iterator ]; } - /** @dataProvider dataIsCloneable */ + #[DataProvider('dataIsCloneable')] public function testIsCloneable(BenevolentUnionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isCloneable(); diff --git a/tests/PHPStan/Type/BitwiseFlagHelperTest.php b/tests/PHPStan/Type/BitwiseFlagHelperTest.php index f2136604c3..e210842df7 100644 --- a/tests/PHPStan/Type/BitwiseFlagHelperTest.php +++ b/tests/PHPStan/Type/BitwiseFlagHelperTest.php @@ -12,13 +12,14 @@ use PHPStan\Analyser\ScopeFactory; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPUnit\Framework\Attributes\DataProvider; use function defined; use function sprintf; final class BitwiseFlagHelperTest extends PHPStanTestCase { - public function dataUnknownConstants(): array + public static function dataUnknownConstants(): array { return [ [ @@ -45,7 +46,7 @@ public function dataUnknownConstants(): array ]; } - public function dataJsonExprContainsConst(): array + public static function dataJsonExprContainsConst(): array { if (!defined('JSON_THROW_ON_ERROR')) { return []; @@ -117,25 +118,25 @@ public function dataJsonExprContainsConst(): array } /** - * @dataProvider dataUnknownConstants - * @dataProvider dataJsonExprContainsConst * * @param non-empty-string $constName */ + #[DataProvider('dataUnknownConstants')] + #[DataProvider('dataJsonExprContainsConst')] public function testExprContainsConst(Expr $expr, string $constName, TrinaryLogic $expected): void { /** @var ScopeFactory $scopeFactory */ $scopeFactory = self::getContainer()->getByType(ScopeFactory::class); $scope = $scopeFactory->create(ScopeContext::create('file.php')) - ->assignVariable('mixedVar', new MixedType(), new MixedType()) - ->assignVariable('stringVar', new StringType(), new StringType()) - ->assignVariable('integerVar', new IntegerType(), new IntegerType()) - ->assignVariable('booleanVar', new BooleanType(), new BooleanType()) - ->assignVariable('floatVar', new FloatType(), new FloatType()) - ->assignVariable('unionIntFloatVar', new UnionType([new IntegerType(), new FloatType()]), new UnionType([new IntegerType(), new FloatType()])) - ->assignVariable('unionStringFloatVar', new UnionType([new StringType(), new FloatType()]), new UnionType([new StringType(), new FloatType()])); + ->assignVariable('mixedVar', new MixedType(), new MixedType(), TrinaryLogic::createYes()) + ->assignVariable('stringVar', new StringType(), new StringType(), TrinaryLogic::createYes()) + ->assignVariable('integerVar', new IntegerType(), new IntegerType(), TrinaryLogic::createYes()) + ->assignVariable('booleanVar', new BooleanType(), new BooleanType(), TrinaryLogic::createYes()) + ->assignVariable('floatVar', new FloatType(), new FloatType(), TrinaryLogic::createYes()) + ->assignVariable('unionIntFloatVar', new UnionType([new IntegerType(), new FloatType()]), new UnionType([new IntegerType(), new FloatType()]), TrinaryLogic::createYes()) + ->assignVariable('unionStringFloatVar', new UnionType([new StringType(), new FloatType()]), new UnionType([new StringType(), new FloatType()]), TrinaryLogic::createYes()); - $analyser = new BitwiseFlagHelper($this->createReflectionProvider()); + $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 375210eea2..c1d189683c 100644 --- a/tests/PHPStan/Type/BooleanTypeTest.php +++ b/tests/PHPStan/Type/BooleanTypeTest.php @@ -6,12 +6,13 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPUnit\Framework\Attributes\DataProvider; use function sprintf; class BooleanTypeTest extends PHPStanTestCase { - public function dataAccepts(): array + public static function dataAccepts(): array { return [ [ @@ -47,12 +48,10 @@ public function dataAccepts(): array ]; } - /** - * @dataProvider dataAccepts - */ + #[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(), @@ -60,7 +59,7 @@ public function testAccepts(BooleanType $type, Type $otherType, TrinaryLogic $ex ); } - public function dataIsSuperTypeOf(): iterable + public static function dataIsSuperTypeOf(): iterable { yield [ new BooleanType(), @@ -93,9 +92,7 @@ public function dataIsSuperTypeOf(): iterable ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(BooleanType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -106,7 +103,7 @@ public function testIsSuperTypeOf(BooleanType $type, Type $otherType, TrinaryLog ); } - public function dataEquals(): array + public static function dataEquals(): array { return [ [ @@ -147,9 +144,7 @@ public function dataEquals(): array ]; } - /** - * @dataProvider dataEquals - */ + #[DataProvider('dataEquals')] public function testEquals(BooleanType $type, Type $otherType, bool $expectedResult): void { $actualResult = $type->equals($otherType); diff --git a/tests/PHPStan/Type/CallableTypeTest.php b/tests/PHPStan/Type/CallableTypeTest.php index 87d3bae274..7ea4c0f30e 100644 --- a/tests/PHPStan/Type/CallableTypeTest.php +++ b/tests/PHPStan/Type/CallableTypeTest.php @@ -16,13 +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 PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ [ @@ -61,9 +62,7 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(CallableType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -74,7 +73,7 @@ public function testIsSuperTypeOf(CallableType $type, Type $otherType, TrinaryLo ); } - public function dataIsSubTypeOf(): array + public static function dataIsSubTypeOf(): array { return [ [ @@ -140,9 +139,7 @@ public function dataIsSubTypeOf(): array ]; } - /** - * @dataProvider dataIsSubTypeOf - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOf(CallableType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSubTypeOf($otherType); @@ -153,9 +150,7 @@ public function testIsSubTypeOf(CallableType $type, Type $otherType, TrinaryLogi ); } - /** - * @dataProvider dataIsSubTypeOf - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOfInversed(CallableType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $otherType->isSuperTypeOf($type); @@ -166,7 +161,7 @@ public function testIsSubTypeOfInversed(CallableType $type, Type $otherType, Tri ); } - public function dataInferTemplateTypes(): array + public static function dataInferTemplateTypes(): array { $param = static fn (Type $type): NativeParameterReflection => new NativeParameterReflection( '', @@ -267,9 +262,9 @@ 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); @@ -280,7 +275,7 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ ); } - public function dataAccepts(): array + public static function dataAccepts(): array { return [ [ @@ -355,66 +350,64 @@ public function dataAccepts(): array TrinaryLogic::createYes(), ], [ - new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), - new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + new CallableType(isPure: TrinaryLogic::createNo()), + new CallableType(isPure: TrinaryLogic::createNo()), TrinaryLogic::createYes(), ], [ - new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), - new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(isPure: TrinaryLogic::createNo()), + new CallableType(isPure: TrinaryLogic::createYes()), TrinaryLogic::createYes(), ], [ - new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), - new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(isPure: TrinaryLogic::createYes()), + new CallableType(isPure: TrinaryLogic::createYes()), TrinaryLogic::createYes(), ], [ - new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), - new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + new CallableType(isPure: TrinaryLogic::createYes()), + new CallableType(isPure: TrinaryLogic::createNo()), TrinaryLogic::createNo(), ], [ - new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), - new CallableType([], new VoidType(), false, null, null, [], 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, null, null, [], TrinaryLogic::createNo()), - new CallableType([], new VoidType(), false, null, null, [], 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, null, null, [], TrinaryLogic::createMaybe()), - new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createMaybe()), + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createYes()), TrinaryLogic::createYes(), ], [ - new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createMaybe()), - new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createMaybe()), + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createNo()), TrinaryLogic::createYes(), ], [ - new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createMaybe()), - new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createMaybe()), + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createMaybe()), + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createMaybe()), TrinaryLogic::createYes(), ], [ - new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), - new CallableType([], new VoidType(), false, null, null, [], 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, null, null, [], TrinaryLogic::createYes()), - new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createYes()), + new CallableType([], new VoidType(), false, isPure: TrinaryLogic::createNo()), TrinaryLogic::createNo(), ], ]; } - /** - * @dataProvider dataAccepts - */ + #[DataProvider('dataAccepts')] public function testAccepts( CallableType $type, Type $acceptedType, @@ -423,7 +416,7 @@ public function testAccepts( { $this->assertSame( $expectedResult->describe(), - $type->accepts($acceptedType, true)->describe(), + $type->accepts($acceptedType, true)->result->describe(), sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/ClassStringTypeTest.php b/tests/PHPStan/Type/ClassStringTypeTest.php index 2d827f7271..bfb87e2fa8 100644 --- a/tests/PHPStan/Type/ClassStringTypeTest.php +++ b/tests/PHPStan/Type/ClassStringTypeTest.php @@ -7,13 +7,14 @@ 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 [ [ @@ -39,9 +40,7 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(ClassStringType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -52,7 +51,7 @@ public function testIsSuperTypeOf(ClassStringType $type, Type $otherType, Trinar ); } - public function dataAccepts(): iterable + public static function dataAccepts(): iterable { yield [ new ClassStringType(), @@ -103,12 +102,10 @@ public function dataAccepts(): iterable ]; } - /** - * @dataProvider dataAccepts - */ + #[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(), @@ -116,7 +113,7 @@ public function testAccepts(ClassStringType $type, Type $otherType, TrinaryLogic ); } - public function dataEquals(): array + public static function dataEquals(): array { return [ [ @@ -132,9 +129,7 @@ public function dataEquals(): array ]; } - /** - * @dataProvider dataEquals - */ + #[DataProvider('dataEquals')] public function testEquals(ClassStringType $type, Type $otherType, bool $expectedResult): void { $actualResult = $type->equals($otherType); diff --git a/tests/PHPStan/Type/ClosureTypeFactoryTest.php b/tests/PHPStan/Type/ClosureTypeFactoryTest.php index 7e3a4537b3..fa2180ea5d 100644 --- a/tests/PHPStan/Type/ClosureTypeFactoryTest.php +++ b/tests/PHPStan/Type/ClosureTypeFactoryTest.php @@ -4,11 +4,12 @@ use Closure; use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class ClosureTypeFactoryTest extends PHPStanTestCase { - public function dataFromClosureObjectReturnType(): array + public static function dataFromClosureObjectReturnType(): array { return [ [static function (): void { @@ -21,8 +22,8 @@ public function dataFromClosureObjectReturnType(): array /** * @param Closure(): mixed $closure - * @dataProvider dataFromClosureObjectReturnType */ + #[DataProvider('dataFromClosureObjectReturnType')] public function testFromClosureObjectReturnType(Closure $closure, string $returnType): void { $closureType = $this->getClosureType($closure); @@ -30,7 +31,7 @@ public function testFromClosureObjectReturnType(Closure $closure, string $return $this->assertSame($returnType, $closureType->getReturnType()->describe(VerbosityLevel::precise())); } - public function dataFromClosureObjectParameter(): array + public static function dataFromClosureObjectParameter(): array { return [ [static function (string $foo): void { @@ -48,8 +49,8 @@ public function dataFromClosureObjectParameter(): array /** * @param Closure(): mixed $closure - * @dataProvider dataFromClosureObjectParameter */ + #[DataProvider('dataFromClosureObjectParameter')] public function testFromClosureObjectParameter(Closure $closure, int $index, string $type): void { $closureType = $this->getClosureType($closure); diff --git a/tests/PHPStan/Type/ClosureTypeTest.php b/tests/PHPStan/Type/ClosureTypeTest.php index ccaaa4ca78..f5ef07239e 100644 --- a/tests/PHPStan/Type/ClosureTypeTest.php +++ b/tests/PHPStan/Type/ClosureTypeTest.php @@ -5,12 +5,13 @@ use Closure; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPUnit\Framework\Attributes\DataProvider; use function sprintf; class ClosureTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ [ @@ -24,7 +25,7 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createYes(), ], [ - new ClosureType([], new MixedType(), false, null, null, null, [], [], []), + new ClosureType([], new MixedType(), false, impurePoints: []), new ClosureType([], new MixedType(), false), TrinaryLogic::createMaybe(), ], @@ -91,9 +92,7 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf( Type $type, Type $otherType, diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php index 0b8bff2efb..2d1aaa237c 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php @@ -5,6 +5,7 @@ use PHPStan\Type\BooleanType; use PHPStan\Type\NullType; use PHPStan\Type\StringType; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; use PHPUnit\Framework\TestCase; @@ -128,4 +129,37 @@ public function testIsList(): void $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 727fd65d01..623c86e3ec 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -18,19 +18,21 @@ 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 PHPStanTestCase { - public function dataAccepts(): iterable + public static function dataAccepts(): iterable { yield [ new ConstantArrayType([], []), @@ -189,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'), @@ -225,7 +227,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new IntegerType(), - ], 0, [1]), + ], optionalKeys: [1]), new ConstantArrayType([ new ConstantStringType('sorton'), new ConstantStringType('limit'), @@ -241,7 +243,7 @@ public function dataAccepts(): iterable new ConstantStringType('limit'), ], [ new IntegerType(), - ], 0, [0]), + ], optionalKeys: [0]), new ConstantArrayType([ new ConstantStringType('limit'), ], [ @@ -255,7 +257,7 @@ public function dataAccepts(): iterable new ConstantStringType('limit'), ], [ new IntegerType(), - ], 0), + ], [0]), new ConstantArrayType([ new ConstantStringType('limit'), ], [ @@ -271,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'), @@ -289,7 +291,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ], 0, [0, 1]), + ], optionalKeys: [0, 1]), new ConstantArrayType([ new ConstantStringType('color'), ], [ @@ -305,7 +307,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ], 0, [0, 1]), + ], optionalKeys: [0, 1]), new ConstantArrayType([ new ConstantStringType('sound'), ], [ @@ -321,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(), ]; @@ -339,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'), @@ -408,12 +410,10 @@ public function dataAccepts(): iterable ]; } - /** - * @dataProvider dataAccepts - */ + #[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(), @@ -421,7 +421,7 @@ public function testAccepts(Type $type, Type $otherType, TrinaryLogic $expectedR ); } - public function dataIsSuperTypeOf(): iterable + public static function dataIsSuperTypeOf(): iterable { yield [ new ConstantArrayType([], []), @@ -522,7 +522,7 @@ public function dataIsSuperTypeOf(): iterable ], [ new IntegerType(), new IntegerType(), - ], 2), + ], [2]), new ConstantArrayType([], []), TrinaryLogic::createNo(), ]; @@ -534,7 +534,7 @@ public function dataIsSuperTypeOf(): iterable ], [ new IntegerType(), new IntegerType(), - ], 2, [0]), + ], [2], [0]), new ConstantArrayType([], []), TrinaryLogic::createNo(), ]; @@ -546,7 +546,7 @@ public function dataIsSuperTypeOf(): iterable ], [ new IntegerType(), new IntegerType(), - ], 2, [0, 1]), + ], [2], [0, 1]), new ConstantArrayType([], []), TrinaryLogic::createYes(), ]; @@ -558,12 +558,12 @@ public function dataIsSuperTypeOf(): iterable ], [ new IntegerType(), new IntegerType(), - ], 2, [0, 1]), + ], [2], [0, 1]), new ConstantArrayType([ new ConstantStringType('foo'), ], [ new IntegerType(), - ], 1, [0]), + ], [1], [0]), TrinaryLogic::createYes(), ]; @@ -579,7 +579,7 @@ public function dataIsSuperTypeOf(): iterable ], [ new IntegerType(), new IntegerType(), - ], 2, [0, 1]), + ], [2], [0, 1]), TrinaryLogic::createMaybe(), ]; @@ -595,7 +595,7 @@ public function dataIsSuperTypeOf(): iterable ], [ new IntegerType(), new IntegerType(), - ], 2, [0, 1]), + ], [2], [0, 1]), TrinaryLogic::createNo(), ]; @@ -606,7 +606,7 @@ public function dataIsSuperTypeOf(): iterable ], [ new IntegerType(), new IntegerType(), - ], 2, [0, 1]), + ], [2], [0, 1]), new ConstantArrayType([ new ConstantStringType('foo'), ], [ @@ -623,7 +623,7 @@ public function dataIsSuperTypeOf(): iterable ], [ new IntegerType(), new IntegerType(), - ], 2, [0, 1]), + ], [2], [0, 1]), TrinaryLogic::createMaybe(), ]; @@ -632,7 +632,7 @@ public function dataIsSuperTypeOf(): iterable new ConstantStringType('foo'), ], [ new IntegerType(), - ], 1, [0]), + ], [1], [0]), new ConstantArrayType([ new ConstantStringType('foo'), ], [ @@ -651,14 +651,48 @@ public function dataIsSuperTypeOf(): iterable new ConstantStringType('foo'), ], [ new IntegerType(), - ], 1, [0]), + ], [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 - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(ConstantArrayType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -669,7 +703,7 @@ public function testIsSuperTypeOf(ConstantArrayType $type, Type $otherType, Trin ); } - public function dataInferTemplateTypes(): array + public static function dataInferTemplateTypes(): array { $templateType = static fn ($name): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), @@ -774,9 +808,9 @@ 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); @@ -787,9 +821,7 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ ); } - /** - * @dataProvider dataIsCallable - */ + #[DataProvider('dataIsCallable')] public function testIsCallable(ConstantArrayType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isCallable(); @@ -800,7 +832,7 @@ public function testIsCallable(ConstantArrayType $type, TrinaryLogic $expectedRe ); } - public function dataIsCallable(): iterable + public static function dataIsCallable(): iterable { yield 'zero items' => [ new ConstantArrayType([], []), @@ -872,7 +904,7 @@ public function dataIsCallable(): iterable ]; } - public function dataValuesArray(): iterable + public static function dataValuesArray(): iterable { yield 'empty' => [ new ConstantArrayType([], []), @@ -886,14 +918,14 @@ public function dataValuesArray(): iterable ], [ new ConstantStringType('a'), new ConstantStringType('b'), - ], [20], [], false), + ], [20], isList: TrinaryLogic::createNo()), new ConstantArrayType([ new ConstantIntegerType(0), new ConstantIntegerType(1), ], [ new ConstantStringType('a'), new ConstantStringType('b'), - ], [2], [], true), + ], [2], isList: TrinaryLogic::createYes()), ]; yield 'optional-1' => [ @@ -909,7 +941,7 @@ public function dataValuesArray(): iterable new ConstantStringType('c'), new ConstantStringType('d'), new ConstantStringType('e'), - ], [15], [1, 3], false), + ], [15], [1, 3], TrinaryLogic::createNo()), new ConstantArrayType([ new ConstantIntegerType(0), new ConstantIntegerType(1), @@ -922,7 +954,7 @@ public function dataValuesArray(): iterable new UnionType([new ConstantStringType('c'), new ConstantStringType('d'), new ConstantStringType('e')]), new UnionType([new ConstantStringType('d'), new ConstantStringType('e')]), new ConstantStringType('e'), - ], [3, 4, 5], [3, 4], true), + ], [3, 4, 5], [3, 4], TrinaryLogic::createYes()), ]; yield 'optional-2' => [ @@ -938,7 +970,7 @@ public function dataValuesArray(): iterable new ConstantStringType('c'), new ConstantStringType('d'), new ConstantStringType('e'), - ], [15], [0, 2, 4], false), + ], [15], [0, 2, 4], TrinaryLogic::createNo()), new ConstantArrayType([ new ConstantIntegerType(0), new ConstantIntegerType(1), @@ -951,7 +983,7 @@ public function dataValuesArray(): iterable new UnionType([new ConstantStringType('c'), new ConstantStringType('d'), new ConstantStringType('e')]), new UnionType([new ConstantStringType('d'), new ConstantStringType('e')]), new ConstantStringType('e'), - ], [2, 3, 4, 5], [2, 3, 4], true), + ], [2, 3, 4, 5], [2, 3, 4], TrinaryLogic::createYes()), ]; yield 'optional-at-end-and-list' => [ @@ -963,7 +995,7 @@ public function dataValuesArray(): iterable new ConstantStringType('a'), new ConstantStringType('b'), new ConstantStringType('c'), - ], [11, 12, 13], [1, 2], true), + ], [11, 12, 13], [1, 2], TrinaryLogic::createYes()), new ConstantArrayType([ new ConstantIntegerType(0), new ConstantIntegerType(1), @@ -972,7 +1004,7 @@ public function dataValuesArray(): iterable new ConstantStringType('a'), new ConstantStringType('b'), new ConstantStringType('c'), - ], [1, 2, 3], [1, 2], true), + ], [1, 2, 3], [1, 2], TrinaryLogic::createYes()), ]; yield 'optional-at-end-but-not-list' => [ @@ -984,7 +1016,7 @@ public function dataValuesArray(): iterable new ConstantStringType('a'), new ConstantStringType('b'), new ConstantStringType('c'), - ], [11, 12, 13], [1, 2], false), + ], [11, 12, 13], [1, 2], TrinaryLogic::createNo()), new ConstantArrayType([ new ConstantIntegerType(0), new ConstantIntegerType(1), @@ -993,13 +1025,11 @@ public function dataValuesArray(): iterable new ConstantStringType('a'), new UnionType([new ConstantStringType('b'), new ConstantStringType('c')]), new ConstantStringType('c'), - ], [1, 2, 3], [1, 2], true), + ], [1, 2, 3], [1, 2], TrinaryLogic::createYes()), ]; } - /** - * @dataProvider dataValuesArray - */ + #[DataProvider('dataValuesArray')] public function testValuesArray(ConstantArrayType $type, ConstantArrayType $expectedType): void { $actualType = $type->getValuesArray(); diff --git a/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php b/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php index 2122e3c829..83260d706c 100644 --- a/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php @@ -4,11 +4,12 @@ use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; class ConstantFloatTypeTest extends PHPStanTestCase { - public function dataDescribe(): array + public static function dataDescribe(): array { return [ [ @@ -38,9 +39,7 @@ public function dataDescribe(): array ]; } - /** - * @dataProvider dataDescribe - */ + #[DataProvider('dataDescribe')] public function testDescribe( ConstantFloatType $type, string $expectedDescription, diff --git a/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php b/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php index 1b39e5ff55..6e4fa1ead7 100644 --- a/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php @@ -7,12 +7,13 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; use function sprintf; class ConstantIntegerTypeTest extends PHPStanTestCase { - public function dataAccepts(): iterable + public static function dataAccepts(): iterable { yield [ new ConstantIntegerType(1), @@ -33,12 +34,10 @@ public function dataAccepts(): iterable ]; } - /** - * @dataProvider dataAccepts - */ + #[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(), @@ -46,7 +45,7 @@ public function testAccepts(ConstantIntegerType $type, Type $otherType, TrinaryL ); } - public function dataIsSuperTypeOf(): iterable + public static function dataIsSuperTypeOf(): iterable { yield [ new ConstantIntegerType(1), @@ -67,9 +66,7 @@ public function dataIsSuperTypeOf(): iterable ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(ConstantIntegerType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); diff --git a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php index 976917737b..d07d72d48a 100644 --- a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php @@ -17,6 +17,7 @@ use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; use stdClass; use Throwable; use function sprintf; @@ -24,9 +25,9 @@ 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), @@ -136,9 +137,7 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(ConstantStringType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -153,14 +152,15 @@ public function testGeneralize(): void { $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-falsy-string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-empty-string&numeric-string', (new ConstantStringType('0'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-falsy-string&numeric-string', (new ConstantStringType('1.123'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType(' 1 1 '))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-falsy-string&numeric-string', (new ConstantStringType('+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType('+1+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-falsy-string&numeric-string', (new ConstantStringType('1e9'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType('1e91e9'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-falsy-string&uppercase-string', (new ConstantStringType('A'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-empty-string&numeric-string&uppercase-string', (new ConstantStringType('0'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string', (new ConstantStringType('1.123'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&uppercase-string', (new ConstantStringType(' 1 1 '))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string', (new ConstantStringType('+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&uppercase-string', (new ConstantStringType('+1+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string', (new ConstantStringType('1e9'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string', (new ConstantStringType('1e91e9'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('string', (new ConstantStringType(''))->generalize(GeneralizePrecision::lessSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::lessSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType(stdClass::class))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); diff --git a/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php b/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php index e083852496..185127d984 100644 --- a/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php +++ b/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php @@ -10,11 +10,12 @@ use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; class OversizedArrayBuilderTest extends PHPStanTestCase { - public function dataBuild(): iterable + public static function dataBuild(): iterable { yield [ '[1, 2, 3]', @@ -28,7 +29,12 @@ public function dataBuild(): iterable yield [ '[1, 2, 3, ...[1, \'foo\' => 2, 3]]', - 'non-empty-array&oversized-array', + 'non-empty-array&oversized-array', + ]; + + yield [ + '[1, 2, 3, ...[1, \'FOO\' => 2, 3]]', + 'non-empty-array&oversized-array', ]; yield [ @@ -49,13 +55,15 @@ public function dataBuild(): iterable ]; yield [ '[1, \'foo\' => 2, 3]', - 'non-empty-array&oversized-array', + 'non-empty-array&oversized-array', + ]; + yield [ + '[1, \'FOO\' => 2, 3]', + 'non-empty-array&oversized-array', ]; } - /** - * @dataProvider dataBuild - */ + #[DataProvider('dataBuild')] public function testBuild(string $sourceCode, string $expectedTypeDescription): void { $parser = self::getParser(); diff --git a/tests/PHPStan/Type/Enum/EnumCaseObjectTypeTest.php b/tests/PHPStan/Type/Enum/EnumCaseObjectTypeTest.php index e700d67a3d..a4e8527c93 100644 --- a/tests/PHPStan/Type/Enum/EnumCaseObjectTypeTest.php +++ b/tests/PHPStan/Type/Enum/EnumCaseObjectTypeTest.php @@ -10,13 +10,14 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; use function sprintf; -use const PHP_VERSION_ID; class EnumCaseObjectTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): iterable + public static function dataIsSuperTypeOf(): iterable { yield [ new ObjectType('PHPStan\Fixture\TestEnum'), @@ -102,14 +103,10 @@ public function dataIsSuperTypeOf(): iterable ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[RequiresPhp('>= 8.1')] + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(Type $type, Type $otherType, TrinaryLogic $expectedResult): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } $actualResult = $type->isSuperTypeOf($otherType); $this->assertSame( $expectedResult->describe(), @@ -118,7 +115,7 @@ public function testIsSuperTypeOf(Type $type, Type $otherType, TrinaryLogic $exp ); } - public function dataAccepts(): iterable + public static function dataAccepts(): iterable { yield [ new ObjectType('PHPStan\Fixture\TestEnum'), @@ -204,22 +201,17 @@ public function dataAccepts(): iterable ]; } - /** - * @dataProvider dataAccepts - */ + #[RequiresPhp('>= 8.1')] + #[DataProvider('dataAccepts')] public function testAccepts( Type $type, Type $acceptedType, TrinaryLogic $expectedResult, ): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $this->assertSame( $expectedResult->describe(), - $type->accepts($acceptedType, true)->describe(), + $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 a5be40c268..d0f5efc7b5 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -3,7 +3,6 @@ namespace PHPStan\Type; use DependentPhpDocs\Foo; -use PHPStan\Broker\Broker; use PHPStan\PhpDoc\Tag\ReturnTag; use PHPStan\ShouldNotHappenException; use PHPStan\Testing\PHPStanTestCase; @@ -161,7 +160,7 @@ public function testFileThrowsPhpDocs(): void public function testFileWithCyclicPhpDocs(): void { - self::getContainer()->getByType(Broker::class); + self::createReflectionProvider(); /** @var FileTypeMapper $fileTypeMapper */ $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); diff --git a/tests/PHPStan/Type/FloatTypeTest.php b/tests/PHPStan/Type/FloatTypeTest.php index ef878bf6dd..878e1d49f4 100644 --- a/tests/PHPStan/Type/FloatTypeTest.php +++ b/tests/PHPStan/Type/FloatTypeTest.php @@ -7,12 +7,13 @@ 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 PHPStanTestCase { - public function dataAccepts(): array + public static function dataAccepts(): array { return [ [ @@ -60,13 +61,11 @@ public function dataAccepts(): array ]; } - /** - * @dataProvider dataAccepts - */ + #[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(), @@ -74,7 +73,7 @@ public function testAccepts(Type $otherType, TrinaryLogic $expectedResult): void ); } - public function dataEquals(): array + public static function dataEquals(): array { return [ [ @@ -120,9 +119,7 @@ public function dataEquals(): array ]; } - /** - * @dataProvider dataEquals - */ + #[DataProvider('dataEquals')] public function testEquals(FloatType $type, Type $otherType, bool $expectedResult): void { $actualResult = $type->equals($otherType); diff --git a/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php b/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php index 3efd727f4c..9f5b836898 100644 --- a/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php +++ b/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php @@ -20,6 +20,7 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; use stdClass; use Throwable; use function sprintf; @@ -27,9 +28,9 @@ class GenericClassStringTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return [ 0 => [ @@ -158,9 +159,7 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(GenericClassStringType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -171,7 +170,7 @@ public function testIsSuperTypeOf(GenericClassStringType $type, Type $otherType, ); } - public function dataAccepts(): array + public static function dataAccepts(): array { return [ 0 => [ @@ -275,16 +274,14 @@ public function dataAccepts(): array ]; } - /** - * @dataProvider dataAccepts - */ + #[DataProvider('dataAccepts')] public function testAccepts( GenericClassStringType $acceptingType, Type $acceptedType, TrinaryLogic $expectedResult, ): void { - $actualResult = $acceptingType->accepts($acceptedType, true); + $actualResult = $acceptingType->accepts($acceptedType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), @@ -292,9 +289,9 @@ public function testAccepts( ); } - public function dataEquals(): array + public static function dataEquals(): array { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return [ [ @@ -320,9 +317,7 @@ public function dataEquals(): array ]; } - /** - * @dataProvider dataEquals - */ + #[DataProvider('dataEquals')] public function testEquals(GenericClassStringType $type, Type $otherType, bool $expected): void { $verbosityLevel = VerbosityLevel::precise(); diff --git a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php index 1012234740..2f3733c363 100644 --- a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php +++ b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php @@ -21,16 +21,18 @@ 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 PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ 'equal type' => [ @@ -134,7 +136,7 @@ public function dataIsSuperTypeOf(): array new GenericObjectType(ReflectionClass::class, [ new ObjectType(stdClass::class), ]), - TrinaryLogic::createYes(), + PHP_VERSION_ID >= 80400 ? TrinaryLogic::createNo() : TrinaryLogic::createYes(), ], [ new GenericObjectType(ReflectionClass::class, [ @@ -143,7 +145,7 @@ public function dataIsSuperTypeOf(): array new GenericObjectType(ReflectionClass::class, [ new ObjectWithoutClassType(), ]), - TrinaryLogic::createMaybe(), + PHP_VERSION_ID >= 80400 ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe(), ], [ new GenericObjectType(ReflectionClass::class, [ @@ -155,63 +157,63 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createNo(), ], [ - new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createInvariant()]), - new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createCovariant()]), + 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')], null, null, [TemplateTypeVariance::createInvariant()]), - new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createContravariant()]), + 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')], null, null, [TemplateTypeVariance::createCovariant()]), - new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createInvariant()]), + 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')], null, null, [TemplateTypeVariance::createCovariant()]), - new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createCovariant()]), + 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')], null, null, [TemplateTypeVariance::createCovariant()]), - new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createContravariant()]), + 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')], null, null, [TemplateTypeVariance::createContravariant()]), - new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createInvariant()]), + 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')], null, null, [TemplateTypeVariance::createContravariant()]), - new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createInvariant()]), + 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')], null, null, [TemplateTypeVariance::createContravariant()]), - new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createCovariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], variances: [TemplateTypeVariance::createContravariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], variances: [TemplateTypeVariance::createCovariant()]), TrinaryLogic::createNo(), ], ]; } - public function dataTypeProjections(): array + public static function dataTypeProjections(): array { - $invariantA = new GenericObjectType(E\Foo::class, [new ObjectType(E\A::class)], null, null, [TemplateTypeVariance::createInvariant()]); - $invariantB = new GenericObjectType(E\Foo::class, [new ObjectType(E\B::class)], null, null, [TemplateTypeVariance::createInvariant()]); - $invariantC = new GenericObjectType(E\Foo::class, [new ObjectType(E\C::class)], null, null, [TemplateTypeVariance::createInvariant()]); + $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)], null, null, [TemplateTypeVariance::createCovariant()]); - $covariantB = new GenericObjectType(E\Foo::class, [new ObjectType(E\B::class)], null, null, [TemplateTypeVariance::createCovariant()]); - $covariantC = new GenericObjectType(E\Foo::class, [new ObjectType(E\C::class)], null, null, [TemplateTypeVariance::createCovariant()]); + $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)], null, null, [TemplateTypeVariance::createContravariant()]); - $contravariantB = new GenericObjectType(E\Foo::class, [new ObjectType(E\B::class)], null, null, [TemplateTypeVariance::createContravariant()]); - $contravariantC = new GenericObjectType(E\Foo::class, [new ObjectType(E\C::class)], null, null, [TemplateTypeVariance::createContravariant()]); + $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)], null, null, [TemplateTypeVariance::createBivariant()]); + $bivariant = new GenericObjectType(E\Foo::class, [new MixedType(true)], variances: [TemplateTypeVariance::createBivariant()]); return [ [$invariantB, $invariantA, TrinaryLogic::createNo()], @@ -260,10 +262,8 @@ public function dataTypeProjections(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - * @dataProvider dataTypeProjections - */ + #[DataProvider('dataIsSuperTypeOf')] + #[DataProvider('dataTypeProjections')] public function testIsSuperTypeOf(Type $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -274,7 +274,7 @@ public function testIsSuperTypeOf(Type $type, Type $otherType, TrinaryLogic $exp ); } - public function dataAccepts(): array + public static function dataAccepts(): array { return [ 'equal type' => [ @@ -330,17 +330,15 @@ public function dataAccepts(): array ]; } - /** - * @dataProvider dataAccepts - * @dataProvider dataTypeProjections - */ + #[DataProvider('dataAccepts')] + #[DataProvider('dataTypeProjections')] public function testAccepts( Type $acceptingType, Type $acceptedType, TrinaryLogic $expectedResult, ): void { - $actualResult = $acceptingType->accepts($acceptedType, true); + $actualResult = $acceptingType->accepts($acceptedType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), @@ -349,7 +347,7 @@ public function testAccepts( } /** @return array}> */ - public function dataInferTemplateTypes(): array + public static function dataInferTemplateTypes(): array { $templateType = static fn ($name, ?Type $bound = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), @@ -448,9 +446,9 @@ 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); @@ -461,8 +459,8 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ ); } - /** @return array}> */ - public function dataGetReferencedTypeArguments(): array + /** @return array}> */ + public static function dataGetReferencedTypeArguments(): array { $templateType = static fn ($name, ?Type $bound = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), @@ -477,7 +475,6 @@ public function dataGetReferencedTypeArguments(): array new GenericObjectType(D\Invariant::class, [ $templateType('T'), ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -485,42 +482,11 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'param: Invariant' => [ - TemplateTypeVariance::createContravariant(), - new GenericObjectType(D\Invariant::class, [ - $templateType('T'), - ], null, null, [ - TemplateTypeVariance::createCovariant(), - ]), - false, - [ - new TemplateTypeReference( - $templateType('T'), - TemplateTypeVariance::createContravariant(), - ), - ], - ], - 'param: Invariant' => [ - TemplateTypeVariance::createContravariant(), - new GenericObjectType(D\Invariant::class, [ - $templateType('T'), - ], null, null, [ - TemplateTypeVariance::createContravariant(), - ]), - false, - [ - new TemplateTypeReference( - $templateType('T'), - TemplateTypeVariance::createCovariant(), - ), - ], - ], 'param: Out' => [ TemplateTypeVariance::createContravariant(), new GenericObjectType(D\Out::class, [ $templateType('T'), ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -535,7 +501,6 @@ public function dataGetReferencedTypeArguments(): array $templateType('T'), ]), ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -552,7 +517,6 @@ public function dataGetReferencedTypeArguments(): array ]), ]), ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -565,7 +529,6 @@ public function dataGetReferencedTypeArguments(): array new GenericObjectType(D\In::class, [ $templateType('T'), ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -580,7 +543,6 @@ public function dataGetReferencedTypeArguments(): array $templateType('T'), ]), ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -597,7 +559,6 @@ public function dataGetReferencedTypeArguments(): array ]), ]), ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -612,7 +573,6 @@ public function dataGetReferencedTypeArguments(): array $templateType('T'), ]), ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -627,84 +587,6 @@ public function dataGetReferencedTypeArguments(): array $templateType('T'), ]), ]), - false, - [ - new TemplateTypeReference( - $templateType('T'), - TemplateTypeVariance::createCovariant(), - ), - ], - ], - 'param: Out>' => [ - TemplateTypeVariance::createContravariant(), - new GenericObjectType(D\Out::class, [ - new GenericObjectType(D\Invariant::class, [ - $templateType('T'), - ]), - ]), - false, - [ - new TemplateTypeReference( - $templateType('T'), - TemplateTypeVariance::createInvariant(), - ), - ], - ], - 'param: In>' => [ - TemplateTypeVariance::createContravariant(), - new GenericObjectType(D\In::class, [ - new GenericObjectType(D\Invariant::class, [ - $templateType('T'), - ]), - ]), - false, - [ - new TemplateTypeReference( - $templateType('T'), - TemplateTypeVariance::createInvariant(), - ), - ], - ], - 'param: Invariant>' => [ - TemplateTypeVariance::createContravariant(), - new GenericObjectType(D\Invariant::class, [ - new GenericObjectType(D\Out::class, [ - $templateType('T'), - ]), - ]), - false, - [ - new TemplateTypeReference( - $templateType('T'), - TemplateTypeVariance::createCovariant(), - ), - ], - ], - 'param: Invariant>' => [ - TemplateTypeVariance::createContravariant(), - new GenericObjectType(D\Invariant::class, [ - new GenericObjectType(D\In::class, [ - $templateType('T'), - ]), - ]), - false, - [ - new TemplateTypeReference( - $templateType('T'), - TemplateTypeVariance::createContravariant(), - ), - ], - ], - 'param: In>>' => [ - TemplateTypeVariance::createContravariant(), - new GenericObjectType(D\In::class, [ - new GenericObjectType(D\Invariant::class, [ - new GenericObjectType(D\Out::class, [ - $templateType('T'), - ]), - ]), - ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -712,29 +594,11 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'param: Out>>' => [ - TemplateTypeVariance::createContravariant(), - new GenericObjectType(D\Out::class, [ - new GenericObjectType(D\Invariant::class, [ - new GenericObjectType(D\In::class, [ - $templateType('T'), - ]), - ]), - ]), - false, - [ - new TemplateTypeReference( - $templateType('T'), - TemplateTypeVariance::createContravariant(), - ), - ], - ], 'return: Invariant' => [ TemplateTypeVariance::createCovariant(), new GenericObjectType(D\Invariant::class, [ $templateType('T'), ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -742,42 +606,11 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'return: Invariant' => [ - TemplateTypeVariance::createCovariant(), - new GenericObjectType(D\Invariant::class, [ - $templateType('T'), - ], null, null, [ - TemplateTypeVariance::createCovariant(), - ]), - false, - [ - new TemplateTypeReference( - $templateType('T'), - TemplateTypeVariance::createCovariant(), - ), - ], - ], - 'return: Invariant' => [ - TemplateTypeVariance::createCovariant(), - new GenericObjectType(D\Invariant::class, [ - $templateType('T'), - ], null, null, [ - TemplateTypeVariance::createContravariant(), - ]), - false, - [ - new TemplateTypeReference( - $templateType('T'), - TemplateTypeVariance::createContravariant(), - ), - ], - ], 'return: Out' => [ TemplateTypeVariance::createCovariant(), new GenericObjectType(D\Out::class, [ $templateType('T'), ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -792,7 +625,6 @@ public function dataGetReferencedTypeArguments(): array $templateType('T'), ]), ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -809,7 +641,6 @@ public function dataGetReferencedTypeArguments(): array ]), ]), ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -822,7 +653,6 @@ public function dataGetReferencedTypeArguments(): array new GenericObjectType(D\In::class, [ $templateType('T'), ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -837,7 +667,6 @@ public function dataGetReferencedTypeArguments(): array $templateType('T'), ]), ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -854,7 +683,6 @@ public function dataGetReferencedTypeArguments(): array ]), ]), ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -869,7 +697,6 @@ public function dataGetReferencedTypeArguments(): array $templateType('T'), ]), ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -884,7 +711,6 @@ public function dataGetReferencedTypeArguments(): array $templateType('T'), ]), ]), - false, [ new TemplateTypeReference( $templateType('T'), @@ -892,108 +718,13 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'return: Out>' => [ - TemplateTypeVariance::createCovariant(), - new GenericObjectType(D\Out::class, [ - new GenericObjectType(D\Invariant::class, [ - $templateType('T'), - ]), - ]), - false, - [ - new TemplateTypeReference( - $templateType('T'), - TemplateTypeVariance::createInvariant(), - ), - ], - ], - 'return: In>' => [ - TemplateTypeVariance::createCovariant(), - new GenericObjectType(D\In::class, [ - new GenericObjectType(D\Invariant::class, [ - $templateType('T'), - ]), - ]), - false, - [ - new TemplateTypeReference( - $templateType('T'), - TemplateTypeVariance::createInvariant(), - ), - ], - ], - 'return: Invariant>' => [ - TemplateTypeVariance::createCovariant(), - new GenericObjectType(D\Invariant::class, [ - new GenericObjectType(D\Out::class, [ - $templateType('T'), - ]), - ]), - false, - [ - new TemplateTypeReference( - $templateType('T'), - TemplateTypeVariance::createCovariant(), - ), - ], - ], - 'return: Invariant>' => [ - TemplateTypeVariance::createCovariant(), - new GenericObjectType(D\Invariant::class, [ - new GenericObjectType(D\In::class, [ - $templateType('T'), - ]), - ]), - false, - [ - new TemplateTypeReference( - $templateType('T'), - TemplateTypeVariance::createContravariant(), - ), - ], - ], - 'return: In>>' => [ - TemplateTypeVariance::createCovariant(), - new GenericObjectType(D\In::class, [ - new GenericObjectType(D\Invariant::class, [ - new GenericObjectType(D\Out::class, [ - $templateType('T'), - ]), - ]), - ]), - false, - [ - new TemplateTypeReference( - $templateType('T'), - TemplateTypeVariance::createCovariant(), - ), - ], - ], - 'return: Out>>' => [ - TemplateTypeVariance::createCovariant(), - new GenericObjectType(D\Out::class, [ - new GenericObjectType(D\Invariant::class, [ - new GenericObjectType(D\In::class, [ - $templateType('T'), - ]), - ]), - ]), - false, - [ - new TemplateTypeReference( - $templateType('T'), - TemplateTypeVariance::createContravariant(), - ), - ], - ], - 'param: Out> (with invariance composition)' => [ + 'param: Out>' => [ TemplateTypeVariance::createContravariant(), new GenericObjectType(D\Out::class, [ new GenericObjectType(D\Invariant::class, [ $templateType('T'), ]), ]), - true, [ new TemplateTypeReference( $templateType('T'), @@ -1001,14 +732,13 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'param: In> (with invariance composition)' => [ + 'param: In>' => [ TemplateTypeVariance::createContravariant(), new GenericObjectType(D\In::class, [ new GenericObjectType(D\Invariant::class, [ $templateType('T'), ]), ]), - true, [ new TemplateTypeReference( $templateType('T'), @@ -1016,14 +746,13 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'param: Invariant> (with invariance composition)' => [ + 'param: Invariant>' => [ TemplateTypeVariance::createContravariant(), new GenericObjectType(D\Invariant::class, [ new GenericObjectType(D\Out::class, [ $templateType('T'), ]), ]), - true, [ new TemplateTypeReference( $templateType('T'), @@ -1031,14 +760,13 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'param: Invariant> (with invariance composition)' => [ + 'param: Invariant>' => [ TemplateTypeVariance::createContravariant(), new GenericObjectType(D\Invariant::class, [ new GenericObjectType(D\In::class, [ $templateType('T'), ]), ]), - true, [ new TemplateTypeReference( $templateType('T'), @@ -1046,7 +774,7 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'param: In>> (with invariance composition)' => [ + 'param: In>>' => [ TemplateTypeVariance::createContravariant(), new GenericObjectType(D\In::class, [ new GenericObjectType(D\Invariant::class, [ @@ -1055,7 +783,6 @@ public function dataGetReferencedTypeArguments(): array ]), ]), ]), - true, [ new TemplateTypeReference( $templateType('T'), @@ -1063,7 +790,7 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'param: Out>> (with invariance composition)' => [ + 'param: Out>>' => [ TemplateTypeVariance::createContravariant(), new GenericObjectType(D\Out::class, [ new GenericObjectType(D\Invariant::class, [ @@ -1072,7 +799,6 @@ public function dataGetReferencedTypeArguments(): array ]), ]), ]), - true, [ new TemplateTypeReference( $templateType('T'), @@ -1080,14 +806,13 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'param: Invariant (with invariance composition)' => [ + 'param: Invariant' => [ TemplateTypeVariance::createContravariant(), new GenericObjectType(D\Invariant::class, [ $templateType('T'), - ], null, null, [ + ], variances: [ TemplateTypeVariance::createCovariant(), ]), - true, [ new TemplateTypeReference( $templateType('T'), @@ -1095,14 +820,13 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'param: Invariant (with invariance composition)' => [ + 'param: Invariant' => [ TemplateTypeVariance::createContravariant(), new GenericObjectType(D\Invariant::class, [ $templateType('T'), - ], null, null, [ + ], variances: [ TemplateTypeVariance::createContravariant(), ]), - true, [ new TemplateTypeReference( $templateType('T'), @@ -1110,14 +834,13 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'return: Out> (with invariance composition)' => [ + 'return: Out>' => [ TemplateTypeVariance::createCovariant(), new GenericObjectType(D\Out::class, [ new GenericObjectType(D\Invariant::class, [ $templateType('T'), ]), ]), - true, [ new TemplateTypeReference( $templateType('T'), @@ -1125,14 +848,13 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'return: In> (with invariance composition)' => [ + 'return: In>' => [ TemplateTypeVariance::createCovariant(), new GenericObjectType(D\In::class, [ new GenericObjectType(D\Invariant::class, [ $templateType('T'), ]), ]), - true, [ new TemplateTypeReference( $templateType('T'), @@ -1140,14 +862,13 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'return: Invariant> (with invariance composition)' => [ + 'return: Invariant>' => [ TemplateTypeVariance::createCovariant(), new GenericObjectType(D\Invariant::class, [ new GenericObjectType(D\Out::class, [ $templateType('T'), ]), ]), - true, [ new TemplateTypeReference( $templateType('T'), @@ -1155,14 +876,13 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'return: Invariant> (with invariance composition)' => [ + 'return: Invariant>' => [ TemplateTypeVariance::createCovariant(), new GenericObjectType(D\Invariant::class, [ new GenericObjectType(D\In::class, [ $templateType('T'), ]), ]), - true, [ new TemplateTypeReference( $templateType('T'), @@ -1170,7 +890,7 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'return: In>> (with invariance composition)' => [ + 'return: In>>' => [ TemplateTypeVariance::createCovariant(), new GenericObjectType(D\In::class, [ new GenericObjectType(D\Invariant::class, [ @@ -1179,7 +899,6 @@ public function dataGetReferencedTypeArguments(): array ]), ]), ]), - true, [ new TemplateTypeReference( $templateType('T'), @@ -1187,7 +906,7 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'return: Out>> (with invariance composition)' => [ + 'return: Out>>' => [ TemplateTypeVariance::createCovariant(), new GenericObjectType(D\Out::class, [ new GenericObjectType(D\Invariant::class, [ @@ -1196,7 +915,6 @@ public function dataGetReferencedTypeArguments(): array ]), ]), ]), - true, [ new TemplateTypeReference( $templateType('T'), @@ -1204,14 +922,13 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'return: Invariant (with invariance composition)' => [ + 'return: Invariant' => [ TemplateTypeVariance::createCovariant(), new GenericObjectType(D\Invariant::class, [ $templateType('T'), - ], null, null, [ + ], variances: [ TemplateTypeVariance::createCovariant(), ]), - true, [ new TemplateTypeReference( $templateType('T'), @@ -1219,14 +936,13 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'return: Invariant (with invariance composition)' => [ + 'return: Invariant' => [ TemplateTypeVariance::createCovariant(), new GenericObjectType(D\Invariant::class, [ $templateType('T'), - ], null, null, [ + ], variances: [ TemplateTypeVariance::createContravariant(), ]), - true, [ new TemplateTypeReference( $templateType('T'), @@ -1238,14 +954,11 @@ public function dataGetReferencedTypeArguments(): array } /** - * @dataProvider dataGetReferencedTypeArguments - * * @param array $expectedReferences */ - public function testGetReferencedTypeArguments(TemplateTypeVariance $positionVariance, Type $type, bool $invarianceComposition, array $expectedReferences): void + #[DataProvider('dataGetReferencedTypeArguments')] + public function testGetReferencedTypeArguments(TemplateTypeVariance $positionVariance, Type $type, array $expectedReferences): void { - TemplateTypeVariance::setInvarianceCompositionEnabled($invarianceComposition); - $result = []; foreach ($type->getReferencedTemplateTypes($positionVariance) as $r) { $result[] = $r; diff --git a/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php b/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php index 2bcde9560a..c54813969d 100644 --- a/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php +++ b/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php @@ -29,7 +29,7 @@ public function testIssue2512(): void TemplateTypeVariance::createInvariant(), ); - $this->assertEquals( + $this->assertSame( 'T (function a(), parameter)', $type->describe(VerbosityLevel::precise()), ); @@ -46,7 +46,7 @@ public function testIssue2512(): void TemplateTypeVariance::createInvariant(), ); - $this->assertEquals( + $this->assertSame( 'DateTime&T (function a(), parameter)', $type->describe(VerbosityLevel::precise()), ); diff --git a/tests/PHPStan/Type/Generic/TemplateTypeMapTest.php b/tests/PHPStan/Type/Generic/TemplateTypeMapTest.php index ff9f5fe58e..c6502e85f6 100644 --- a/tests/PHPStan/Type/Generic/TemplateTypeMapTest.php +++ b/tests/PHPStan/Type/Generic/TemplateTypeMapTest.php @@ -6,12 +6,13 @@ use InvalidArgumentException; use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class TemplateTypeMapTest extends TestCase { - public function dataUnionWithLowerBoundTypes(): iterable + public static function dataUnionWithLowerBoundTypes(): iterable { $map = (new TemplateTypeMap([ 'T' => new ObjectType(Exception::class), @@ -55,7 +56,7 @@ public function dataUnionWithLowerBoundTypes(): iterable ]; } - /** @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 71a57dda6d..107ca6963f 100644 --- a/tests/PHPStan/Type/Generic/TemplateTypeVarianceTest.php +++ b/tests/PHPStan/Type/Generic/TemplateTypeVarianceTest.php @@ -9,13 +9,14 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use function sprintf; class TemplateTypeVarianceTest extends TestCase { - public function dataIsValidVariance(): iterable + public static function dataIsValidVariance(): iterable { foreach ([TemplateTypeVariance::createInvariant(), TemplateTypeVariance::createCovariant()] as $variance) { yield [ @@ -76,9 +77,7 @@ public function dataIsValidVariance(): iterable } } - /** - * @dataProvider dataIsValidVariance - */ + #[DataProvider('dataIsValidVariance')] public function testIsValidVariance( TemplateTypeVariance $variance, Type $a, @@ -87,14 +86,15 @@ public function testIsValidVariance( TrinaryLogic $expectedInversed, ): void { + $templateType = TemplateTypeFactory::create(TemplateTypeScope::createWithFunction('foo'), 'T', null, $variance); $this->assertSame( $expected->describe(), - $variance->isValidVariance($a, $b)->describe(), + $variance->isValidVariance($templateType, $a, $b)->result->describe(), sprintf('%s->isValidVariance(%s, %s)', $variance->describe(), $a->describe(VerbosityLevel::precise()), $b->describe(VerbosityLevel::precise())), ); $this->assertSame( $expectedInversed->describe(), - $variance->isValidVariance($b, $a)->describe(), + $variance->isValidVariance($templateType, $b, $a)->result->describe(), sprintf('%s->isValidVariance(%s, %s)', $variance->describe(), $b->describe(VerbosityLevel::precise()), $a->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/IntegerTypeTest.php b/tests/PHPStan/Type/IntegerTypeTest.php index a1c83f7885..e6512b6f8c 100644 --- a/tests/PHPStan/Type/IntegerTypeTest.php +++ b/tests/PHPStan/Type/IntegerTypeTest.php @@ -7,12 +7,13 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPUnit\Framework\Attributes\DataProvider; use function sprintf; class IntegerTypeTest extends PHPStanTestCase { - public function dataAccepts(): array + public static function dataAccepts(): array { return [ [ @@ -48,12 +49,10 @@ public function dataAccepts(): array ]; } - /** - * @dataProvider dataAccepts - */ + #[DataProvider('dataAccepts')] public function testAccepts(IntegerType $type, Type $otherType, TrinaryLogic $expectedResult): void { - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), @@ -61,7 +60,7 @@ public function testAccepts(IntegerType $type, Type $otherType, TrinaryLogic $ex ); } - public function dataIsSuperTypeOf(): iterable + public static function dataIsSuperTypeOf(): iterable { yield [ new IntegerType(), @@ -94,9 +93,7 @@ public function dataIsSuperTypeOf(): iterable ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(IntegerType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -107,7 +104,7 @@ public function testIsSuperTypeOf(IntegerType $type, Type $otherType, TrinaryLog ); } - public function dataEquals(): array + public static function dataEquals(): array { return [ [ @@ -153,9 +150,7 @@ public function dataEquals(): array ]; } - /** - * @dataProvider dataEquals - */ + #[DataProvider('dataEquals')] public function testEquals(IntegerType $type, Type $otherType, bool $expectedResult): void { $actualResult = $type->equals($otherType); diff --git a/tests/PHPStan/Type/IntersectionTypeTest.php b/tests/PHPStan/Type/IntersectionTypeTest.php index aae9faa0bd..80f6c21b95 100644 --- a/tests/PHPStan/Type/IntersectionTypeTest.php +++ b/tests/PHPStan/Type/IntersectionTypeTest.php @@ -7,7 +7,9 @@ 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; @@ -15,6 +17,7 @@ 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; @@ -25,7 +28,7 @@ class IntersectionTypeTest extends PHPStanTestCase { - public function dataAccepts(): Iterator + public static function dataAccepts(): Iterator { $intersectionType = new IntersectionType([ new ObjectType('Collection'), @@ -66,12 +69,10 @@ public function dataAccepts(): Iterator ]; } - /** - * @dataProvider dataAccepts - */ + #[DataProvider('dataAccepts')] public function testAccepts(IntersectionType $type, Type $otherType, TrinaryLogic $expectedResult): void { - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), @@ -79,7 +80,7 @@ public function testAccepts(IntersectionType $type, Type $otherType, TrinaryLogi ); } - public function dataIsCallable(): array + public static function dataIsCallable(): array { return [ [ @@ -109,9 +110,7 @@ public function dataIsCallable(): array ]; } - /** - * @dataProvider dataIsCallable - */ + #[DataProvider('dataIsCallable')] public function testIsCallable(IntersectionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isCallable(); @@ -122,7 +121,7 @@ public function testIsCallable(IntersectionType $type, TrinaryLogic $expectedRes ); } - public function dataIsSuperTypeOf(): Iterator + public static function dataIsSuperTypeOf(): Iterator { $intersectionTypeA = new IntersectionType([ new ObjectType('ArrayObject'), @@ -153,52 +152,6 @@ public function dataIsSuperTypeOf(): Iterator TrinaryLogic::createNo(), ]; - yield [ - new IntersectionType([ - new ArrayType(new MixedType(), new MixedType()), - new HasOffsetType(new StringType()), - ]), - new ConstantArrayType([ - new ConstantStringType('a'), - new ConstantStringType('b'), - new ConstantStringType('c'), - ], [ - new ConstantIntegerType(1), - new ConstantIntegerType(2), - new ConstantIntegerType(3), - ]), - TrinaryLogic::createMaybe(), - ]; - - yield [ - new IntersectionType([ - new ArrayType(new MixedType(), new MixedType()), - new HasOffsetType(new StringType()), - ]), - new ConstantArrayType([ - new ConstantStringType('a'), - new ConstantStringType('b'), - new ConstantStringType('c'), - new ConstantStringType('d'), - new ConstantStringType('e'), - new ConstantStringType('f'), - new ConstantStringType('g'), - new ConstantStringType('h'), - new ConstantStringType('i'), - ], [ - new ConstantIntegerType(1), - new ConstantIntegerType(2), - new ConstantIntegerType(3), - new ConstantIntegerType(1), - new ConstantIntegerType(2), - new ConstantIntegerType(3), - new ConstantIntegerType(1), - new ConstantIntegerType(2), - new ConstantIntegerType(3), - ]), - TrinaryLogic::createMaybe(), - ]; - yield [ new IntersectionType([ new ObjectType(Traversable::class), @@ -278,9 +231,7 @@ public function dataIsSuperTypeOf(): Iterator ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(IntersectionType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -291,7 +242,7 @@ public function testIsSuperTypeOf(IntersectionType $type, Type $otherType, Trina ); } - public function dataIsSubTypeOf(): Iterator + public static function dataIsSubTypeOf(): Iterator { $intersectionTypeA = new IntersectionType([ new ObjectType('ArrayObject'), @@ -378,9 +329,7 @@ public function dataIsSubTypeOf(): Iterator ]; } - /** - * @dataProvider dataIsSubTypeOf - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOf(IntersectionType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSubTypeOf($otherType); @@ -391,9 +340,7 @@ public function testIsSubTypeOf(IntersectionType $type, Type $otherType, Trinary ); } - /** - * @dataProvider dataIsSubTypeOf - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOfInversed(IntersectionType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $otherType->isSuperTypeOf($type); @@ -410,13 +357,13 @@ public function testToBooleanCrash(): void $this->assertSame('true', $type->toBoolean()->describe(VerbosityLevel::precise())); } - public function dataGetEnumCases(): iterable + public static function dataGetEnumCases(): iterable { if (PHP_VERSION_ID < 80100) { return []; } - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $classReflection = $reflectionProvider->getClass(FooEnum::class); yield [ @@ -431,9 +378,9 @@ public function dataGetEnumCases(): iterable } /** - * @dataProvider dataGetEnumCases * @param list $expectedEnumCases */ + #[DataProvider('dataGetEnumCases')] public function testGetEnumCases( IntersectionType $type, array $expectedEnumCases, @@ -447,4 +394,330 @@ public function testGetEnumCases( } } + 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 2094ebf589..557caeb1ad 100644 --- a/tests/PHPStan/Type/IterableTypeTest.php +++ b/tests/PHPStan/Type/IterableTypeTest.php @@ -11,13 +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 PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ [ @@ -58,9 +59,7 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(IterableType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -71,7 +70,7 @@ public function testIsSuperTypeOf(IterableType $type, Type $otherType, TrinaryLo ); } - public function dataIsSubTypeOf(): array + public static function dataIsSubTypeOf(): array { return [ [ @@ -157,9 +156,7 @@ public function dataIsSubTypeOf(): array ]; } - /** - * @dataProvider dataIsSubTypeOf - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOf(IterableType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSubTypeOf($otherType); @@ -170,9 +167,7 @@ public function testIsSubTypeOf(IterableType $type, Type $otherType, TrinaryLogi ); } - /** - * @dataProvider dataIsSubTypeOf - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOfInversed(IterableType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $otherType->isSuperTypeOf($type); @@ -183,7 +178,7 @@ public function testIsSubTypeOfInversed(IterableType $type, Type $otherType, Tri ); } - public function dataInferTemplateTypes(): array + public static function dataInferTemplateTypes(): array { $templateType = static fn ($name): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), @@ -235,9 +230,9 @@ 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); @@ -248,7 +243,7 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ ); } - public function dataDescribe(): array + public static function dataDescribe(): array { $templateType = TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), @@ -285,9 +280,7 @@ public function dataDescribe(): array ]; } - /** - * @dataProvider dataDescribe - */ + #[DataProvider('dataDescribe')] public function testDescribe(Type $type, string $expect): void { $result = $type->describe(VerbosityLevel::typeOnly()); @@ -295,7 +288,7 @@ 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( @@ -324,12 +317,10 @@ public function dataAccepts(): array ]; } - /** - * @dataProvider dataAccepts - */ + #[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(), diff --git a/tests/PHPStan/Type/MixedTypeTest.php b/tests/PHPStan/Type/MixedTypeTest.php index 5fe7d0c409..20baf951f9 100644 --- a/tests/PHPStan/Type/MixedTypeTest.php +++ b/tests/PHPStan/Type/MixedTypeTest.php @@ -13,12 +13,13 @@ 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 PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ 0 => [ @@ -32,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 => [ @@ -82,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 => [ @@ -137,7 +138,7 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createYes(), ], 23 => [ - new MixedType(false, new NullType()), + new MixedType(subtractedType: new NullType()), new NeverType(), TrinaryLogic::createYes(), ], @@ -147,7 +148,7 @@ 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(), ], @@ -159,9 +160,7 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(MixedType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -172,7 +171,7 @@ public function testIsSuperTypeOf(MixedType $type, Type $otherType, TrinaryLogic ); } - public function dataSubstractedIsArray(): array + public static function dataSubstractedIsArray(): array { return [ [ @@ -226,9 +225,7 @@ public function dataSubstractedIsArray(): array ]; } - /** - * @dataProvider dataSubstractedIsArray - */ + #[DataProvider('dataSubstractedIsArray')] public function testSubstractedIsArray(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -241,7 +238,7 @@ public function testSubstractedIsArray(MixedType $mixedType, Type $typeToSubtrac ); } - public function dataSubstractedIsConstantArray(): array + public static function dataSubstractedIsConstantArray(): array { return [ [ @@ -300,9 +297,7 @@ public function dataSubstractedIsConstantArray(): array ]; } - /** - * @dataProvider dataSubstractedIsConstantArray - */ + #[DataProvider('dataSubstractedIsConstantArray')] public function testSubstractedIsConstantArray(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -315,7 +310,7 @@ public function testSubstractedIsConstantArray(MixedType $mixedType, Type $typeT ); } - public function dataSubstractedIsString(): array + public static function dataSubstractedIsString(): array { return [ [ @@ -352,9 +347,7 @@ public function dataSubstractedIsString(): array ]; } - /** - * @dataProvider dataSubstractedIsString - */ + #[DataProvider('dataSubstractedIsString')] public function testSubstractedIsString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -367,7 +360,7 @@ public function testSubstractedIsString(MixedType $mixedType, Type $typeToSubtra ); } - public function dataSubstractedIsNumericString(): array + public static function dataSubstractedIsNumericString(): array { return [ [ @@ -404,9 +397,7 @@ public function dataSubstractedIsNumericString(): array ]; } - /** - * @dataProvider dataSubstractedIsNumericString - */ + #[DataProvider('dataSubstractedIsNumericString')] public function testSubstractedIsNumericString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -419,7 +410,7 @@ public function testSubstractedIsNumericString(MixedType $mixedType, Type $typeT ); } - public function dataSubstractedIsNonEmptyString(): array + public static function dataSubstractedIsNonEmptyString(): array { return [ [ @@ -464,9 +455,7 @@ public function dataSubstractedIsNonEmptyString(): array ]; } - /** - * @dataProvider dataSubstractedIsNonEmptyString - */ + #[DataProvider('dataSubstractedIsNonEmptyString')] public function testSubstractedIsNonEmptyString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -479,7 +468,7 @@ public function testSubstractedIsNonEmptyString(MixedType $mixedType, Type $type ); } - public function dataSubstractedIsNonFalsyString(): array + public static function dataSubstractedIsNonFalsyString(): array { return [ [ @@ -524,9 +513,7 @@ public function dataSubstractedIsNonFalsyString(): array ]; } - /** - * @dataProvider dataSubstractedIsNonFalsyString - */ + #[DataProvider('dataSubstractedIsNonFalsyString')] public function testSubstractedIsNonFalsyString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -539,7 +526,7 @@ public function testSubstractedIsNonFalsyString(MixedType $mixedType, Type $type ); } - public function dataSubstractedIsLiteralString(): array + public static function dataSubstractedIsLiteralString(): array { return [ [ @@ -592,13 +579,11 @@ public function dataSubstractedIsLiteralString(): array ]; } - /** - * @dataProvider dataSubstractedIsClassString - */ + #[DataProvider('dataSubstractedIsClassString')] public function testSubstractedIsClassString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); - $actualResult = $subtracted->isClassStringType(); + $actualResult = $subtracted->isClassString(); $this->assertSame( $expectedResult->describe(), @@ -607,7 +592,7 @@ public function testSubstractedIsClassString(MixedType $mixedType, Type $typeToS ); } - public function dataSubstractedIsClassString(): array + public static function dataSubstractedIsClassString(): array { return [ [ @@ -636,7 +621,7 @@ public function dataSubstractedIsClassString(): array ]; } - /** @dataProvider dataSubtractedIsVoid */ + #[DataProvider('dataSubtractedIsVoid')] public function testSubtractedIsVoid(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -649,7 +634,7 @@ public function testSubtractedIsVoid(MixedType $mixedType, Type $typeToSubtract, ); } - public function dataSubtractedIsVoid(): array + public static function dataSubtractedIsVoid(): array { return [ [ @@ -665,7 +650,7 @@ public function dataSubtractedIsVoid(): array ]; } - /** @dataProvider dataSubtractedIsScalar */ + #[DataProvider('dataSubtractedIsScalar')] public function testSubtractedIsScalar(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -678,7 +663,7 @@ public function testSubtractedIsScalar(MixedType $mixedType, Type $typeToSubtrac ); } - public function dataSubtractedIsScalar(): array + public static function dataSubtractedIsScalar(): array { return [ [ @@ -694,9 +679,7 @@ public function dataSubtractedIsScalar(): array ]; } - /** - * @dataProvider dataSubstractedIsLiteralString - */ + #[DataProvider('dataSubstractedIsLiteralString')] public function testSubstractedIsLiteralString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -709,7 +692,7 @@ public function testSubstractedIsLiteralString(MixedType $mixedType, Type $typeT ); } - public function dataSubstractedIsIterable(): array + public static function dataSubstractedIsIterable(): array { return [ [ @@ -748,9 +731,7 @@ public function dataSubstractedIsIterable(): array ]; } - /** - * @dataProvider dataSubstractedIsBoolean - */ + #[DataProvider('dataSubstractedIsBoolean')] public function testSubstractedIsBoolean(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -763,7 +744,7 @@ public function testSubstractedIsBoolean(MixedType $mixedType, Type $typeToSubtr ); } - public function dataSubstractedIsBoolean(): array + public static function dataSubstractedIsBoolean(): array { return [ [ @@ -789,9 +770,7 @@ public function dataSubstractedIsBoolean(): array ]; } - /** - * @dataProvider dataSubstractedIsFalse - */ + #[DataProvider('dataSubstractedIsFalse')] public function testSubstractedIsFalse(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -804,7 +783,7 @@ public function testSubstractedIsFalse(MixedType $mixedType, Type $typeToSubtrac ); } - public function dataSubstractedIsFalse(): array + public static function dataSubstractedIsFalse(): array { return [ [ @@ -830,9 +809,7 @@ public function dataSubstractedIsFalse(): array ]; } - /** - * @dataProvider dataSubstractedIsNull - */ + #[DataProvider('dataSubstractedIsNull')] public function testSubstractedIsNull(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -845,7 +822,7 @@ public function testSubstractedIsNull(MixedType $mixedType, Type $typeToSubtract ); } - public function dataSubstractedIsNull(): array + public static function dataSubstractedIsNull(): array { return [ [ @@ -876,9 +853,7 @@ public function dataSubstractedIsNull(): array ]; } - /** - * @dataProvider dataSubstractedIsTrue - */ + #[DataProvider('dataSubstractedIsTrue')] public function testSubstractedIsTrue(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -891,7 +866,7 @@ public function testSubstractedIsTrue(MixedType $mixedType, Type $typeToSubtract ); } - public function dataSubstractedIsTrue(): array + public static function dataSubstractedIsTrue(): array { return [ [ @@ -917,9 +892,7 @@ public function dataSubstractedIsTrue(): array ]; } - /** - * @dataProvider dataSubstractedIsFloat - */ + #[DataProvider('dataSubstractedIsFloat')] public function testSubstractedIsFloat(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -932,7 +905,7 @@ public function testSubstractedIsFloat(MixedType $mixedType, Type $typeToSubtrac ); } - public function dataSubstractedIsFloat(): array + public static function dataSubstractedIsFloat(): array { return [ [ @@ -953,9 +926,7 @@ public function dataSubstractedIsFloat(): array ]; } - /** - * @dataProvider dataSubstractedIsInteger - */ + #[DataProvider('dataSubstractedIsInteger')] public function testSubstractedIsInteger(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -968,7 +939,7 @@ public function testSubstractedIsInteger(MixedType $mixedType, Type $typeToSubtr ); } - public function dataSubstractedIsInteger(): array + public static function dataSubstractedIsInteger(): array { return [ [ @@ -989,9 +960,7 @@ public function dataSubstractedIsInteger(): array ]; } - /** - * @dataProvider dataSubstractedIsIterable - */ + #[DataProvider('dataSubstractedIsIterable')] public function testSubstractedIsIterable(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -1004,7 +973,7 @@ public function testSubstractedIsIterable(MixedType $mixedType, Type $typeToSubt ); } - public function dataSubstractedIsOffsetAccessible(): array + public static function dataSubstractedIsOffsetAccessible(): array { return [ [ @@ -1044,9 +1013,7 @@ public function dataSubstractedIsOffsetAccessible(): array ]; } - /** - * @dataProvider dataSubstractedIsOffsetAccessible - */ + #[DataProvider('dataSubstractedIsOffsetAccessible')] public function testSubstractedIsOffsetAccessible(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -1059,7 +1026,7 @@ public function testSubstractedIsOffsetAccessible(MixedType $mixedType, Type $ty ); } - public function dataSubstractedIsOffsetLegal(): array + public static function dataSubstractedIsOffsetLegal(): array { return [ [ @@ -1091,9 +1058,7 @@ public function dataSubstractedIsOffsetLegal(): array ]; } - /** - * @dataProvider dataSubstractedIsOffsetLegal - */ + #[DataProvider('dataSubstractedIsOffsetLegal')] public function testSubstractedIsOffsetLegal(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -1106,7 +1071,7 @@ public function testSubstractedIsOffsetLegal(MixedType $mixedType, Type $typeToS ); } - public function dataSubtractedHasOffsetValueType(): array + public static function dataSubtractedHasOffsetValueType(): array { return [ [ @@ -1151,7 +1116,7 @@ public function dataSubtractedHasOffsetValueType(): array ]; } - /** @dataProvider dataSubtractedHasOffsetValueType */ + #[DataProvider('dataSubtractedHasOffsetValueType')] public function testSubtractedHasOffsetValueType(MixedType $mixedType, Type $typeToSubtract, Type $offsetType, TrinaryLogic $expectedResult): void { $subtracted = $mixedType->subtract($typeToSubtract); @@ -1164,4 +1129,50 @@ public function testSubtractedHasOffsetValueType(MixedType $mixedType, Type $typ ); } + /** @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 6d8cfb28ee..253e9f0c3a 100644 --- a/tests/PHPStan/Type/ObjectTypeTest.php +++ b/tests/PHPStan/Type/ObjectTypeTest.php @@ -35,6 +35,8 @@ 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; @@ -47,7 +49,7 @@ class ObjectTypeTest extends PHPStanTestCase { - public function dataIsIterable(): array + public static function dataIsIterable(): array { return [ [new ObjectType('ArrayObject'), TrinaryLogic::createYes()], @@ -57,9 +59,7 @@ public function dataIsIterable(): array ]; } - /** - * @dataProvider dataIsIterable - */ + #[DataProvider('dataIsIterable')] public function testIsIterable(ObjectType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isIterable(); @@ -70,7 +70,34 @@ public function testIsIterable(ObjectType $type, TrinaryLogic $expectedResult): ); } - 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()], @@ -79,9 +106,7 @@ public function dataIsCallable(): array ]; } - /** - * @dataProvider dataIsCallable - */ + #[DataProvider('dataIsCallable')] public function testIsCallable(ObjectType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isCallable(); @@ -92,9 +117,9 @@ public function testIsCallable(ObjectType $type, TrinaryLogic $expectedResult): ); } - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return [ 0 => [ @@ -456,9 +481,7 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(ObjectType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -469,7 +492,7 @@ public function testIsSuperTypeOf(ObjectType $type, Type $otherType, TrinaryLogi ); } - public function dataAccepts(): array + public static function dataAccepts(): array { return [ [ @@ -520,9 +543,7 @@ public function dataAccepts(): array ]; } - /** - * @dataProvider dataAccepts - */ + #[DataProvider('dataAccepts')] public function testAccepts( ObjectType $type, Type $acceptedType, @@ -531,11 +552,46 @@ public function testAccepts( { $this->assertSame( $expectedResult->describe(), - $type->accepts($acceptedType, true)->describe(), + $type->accepts($acceptedType, true)->result->describe(), sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())), ); } + 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); @@ -544,7 +600,7 @@ public function testGetClassReflectionOfGenericClass(): void $this->assertSame('Traversable', $classReflection->getDisplayName()); } - public function dataHasOffsetValueType(): array + public static function dataHasOffsetValueType(): array { return [ [ @@ -600,9 +656,7 @@ public function dataHasOffsetValueType(): array ]; } - /** - * @dataProvider dataHasOffsetValueType - */ + #[DataProvider('dataHasOffsetValueType')] public function testHasOffsetValueType( ObjectType $type, Type $offsetType, @@ -616,7 +670,7 @@ public function testHasOffsetValueType( ); } - public function dataGetEnumCases(): iterable + public static function dataGetEnumCases(): iterable { yield [ new ObjectType(stdClass::class), @@ -649,18 +703,15 @@ public function dataGetEnumCases(): iterable } /** - * @dataProvider dataGetEnumCases * @param list $expectedEnumCases */ + #[RequiresPhp('>= 8.1')] + #[DataProvider('dataGetEnumCases')] public function testGetEnumCases( ObjectType $type, array $expectedEnumCases, ): void { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Test requires PHP 8.1.'); - } - $enumCases = $type->getEnumCases(); $this->assertCount(count($expectedEnumCases), $enumCases); foreach ($enumCases as $i => $enumCase) { diff --git a/tests/PHPStan/Type/ObjectWithoutClassTypeTest.php b/tests/PHPStan/Type/ObjectWithoutClassTypeTest.php index c5fde900ce..69e33b253a 100644 --- a/tests/PHPStan/Type/ObjectWithoutClassTypeTest.php +++ b/tests/PHPStan/Type/ObjectWithoutClassTypeTest.php @@ -5,12 +5,13 @@ use InvalidArgumentException; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPUnit\Framework\Attributes\DataProvider; use function sprintf; class ObjectWithoutClassTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ [ @@ -61,9 +62,7 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(ObjectWithoutClassType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); diff --git a/tests/PHPStan/Type/Php/CurlInitReturnTypeExtensionTest.php b/tests/PHPStan/Type/Php/CurlInitReturnTypeExtensionTest.php deleted file mode 100644 index 7749b327f1..0000000000 --- a/tests/PHPStan/Type/Php/CurlInitReturnTypeExtensionTest.php +++ /dev/null @@ -1,32 +0,0 @@ -assertFileAsserts($assertType, $file, ...$args); - } - -} diff --git a/tests/PHPStan/Type/Php/data/curl-init-php-7.php b/tests/PHPStan/Type/Php/data/curl-init-php-7.php deleted file mode 100644 index 9778b786e0..0000000000 --- a/tests/PHPStan/Type/Php/data/curl-init-php-7.php +++ /dev/null @@ -1,65 +0,0 @@ -getByType(RegexExpressionHelper::class); + + $this->assertSame( + $expectedPatternWithoutDelimiter, + $regexExpressionHelper->removeDelimitersAndModifiers($inputPattern), + ); + } + +} diff --git a/tests/PHPStan/Type/SimultaneousTypeTraverserTest.php b/tests/PHPStan/Type/SimultaneousTypeTraverserTest.php index e9cb8ada70..f7be3c75f7 100644 --- a/tests/PHPStan/Type/SimultaneousTypeTraverserTest.php +++ b/tests/PHPStan/Type/SimultaneousTypeTraverserTest.php @@ -6,11 +6,12 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPUnit\Framework\Attributes\DataProvider; class SimultaneousTypeTraverserTest extends PHPStanTestCase { - public function dataChangeStringIntoNonEmptyString(): iterable + public static function dataChangeStringIntoNonEmptyString(): iterable { yield [ new StringType(), @@ -22,7 +23,7 @@ public function dataChangeStringIntoNonEmptyString(): iterable new ConstantArrayType( [new ConstantIntegerType(0)], [new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()])], - 1, + [1], ), 'array', ]; @@ -31,7 +32,7 @@ public function dataChangeStringIntoNonEmptyString(): iterable new ConstantArrayType( [new ConstantIntegerType(0)], [new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()])], - 1, + [1], ), 'array', ]; @@ -40,7 +41,7 @@ public function dataChangeStringIntoNonEmptyString(): iterable new ConstantArrayType( [new ConstantIntegerType(0)], [new IntegerType()], - 1, + [1], ), 'array', ]; @@ -71,9 +72,7 @@ public function dataChangeStringIntoNonEmptyString(): iterable ]; } - /** - * @dataProvider dataChangeStringIntoNonEmptyString - */ + #[DataProvider('dataChangeStringIntoNonEmptyString')] public function testChangeIntegerIntoString(Type $left, Type $right, string $expectedTypeDescription): void { $cb = static function (Type $left, Type $right, callable $traverse): Type { diff --git a/tests/PHPStan/Type/StaticTypeTest.php b/tests/PHPStan/Type/StaticTypeTest.php index a462bda476..980b5f6004 100644 --- a/tests/PHPStan/Type/StaticTypeTest.php +++ b/tests/PHPStan/Type/StaticTypeTest.php @@ -10,8 +10,15 @@ 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; @@ -22,9 +29,9 @@ 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()], @@ -33,9 +40,7 @@ public function dataIsIterable(): array ]; } - /** - * @dataProvider dataIsIterable - */ + #[DataProvider('dataIsIterable')] public function testIsIterable(StaticType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isIterable(); @@ -46,9 +51,9 @@ public function testIsIterable(StaticType $type, TrinaryLogic $expectedResult): ); } - public function dataIsCallable(): array + public static function dataIsCallable(): array { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return [ [new StaticType($reflectionProvider->getClass('Closure')), TrinaryLogic::createYes()], @@ -56,9 +61,7 @@ public function dataIsCallable(): array ]; } - /** - * @dataProvider dataIsCallable - */ + #[DataProvider('dataIsCallable')] public function testIsCallable(StaticType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isCallable(); @@ -69,9 +72,9 @@ public function testIsCallable(StaticType $type, TrinaryLogic $expectedResult): ); } - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return [ 1 => [ new StaticType($reflectionProvider->getClass(ArrayAccess::class)), @@ -270,12 +273,22 @@ public function dataIsSuperTypeOf(): array ]), 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 - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(Type $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -286,9 +299,9 @@ public function testIsSuperTypeOf(Type $type, Type $otherType, TrinaryLogic $exp ); } - public function dataEquals(): array + public static function dataEquals(): array { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); return [ [ @@ -314,13 +327,136 @@ public function dataEquals(): array ]; } - /** - * @dataProvider dataEquals - */ + #[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 8f22550446..a5630c1a30 100644 --- a/tests/PHPStan/Type/StringTypeTest.php +++ b/tests/PHPStan/Type/StringTypeTest.php @@ -11,6 +11,7 @@ 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; @@ -18,7 +19,7 @@ class StringTypeTest extends PHPStanTestCase { - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { return [ [ @@ -94,9 +95,7 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(StringType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -107,7 +106,7 @@ public function testIsSuperTypeOf(StringType $type, Type $otherType, TrinaryLogi ); } - public function dataAccepts(): iterable + public static function dataAccepts(): iterable { yield [ new StringType(), @@ -174,12 +173,10 @@ public function dataAccepts(): iterable ]; } - /** - * @dataProvider dataAccepts - */ + #[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(), @@ -187,7 +184,7 @@ public function testAccepts(StringType $type, Type $otherType, TrinaryLogic $exp ); } - public function dataEquals(): array + public static function dataEquals(): array { return [ [ @@ -223,9 +220,7 @@ public function dataEquals(): array ]; } - /** - * @dataProvider dataEquals - */ + #[DataProvider('dataEquals')] public function testEquals(StringType $type, Type $otherType, bool $expectedResult): void { $actualResult = $type->equals($otherType); diff --git a/tests/PHPStan/Type/TemplateTypeTest.php b/tests/PHPStan/Type/TemplateTypeTest.php index 3a17311989..61c8839f13 100644 --- a/tests/PHPStan/Type/TemplateTypeTest.php +++ b/tests/PHPStan/Type/TemplateTypeTest.php @@ -13,6 +13,7 @@ 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; @@ -23,7 +24,7 @@ class TemplateTypeTest extends PHPStanTestCase { - public function dataAccepts(): array + public static function dataAccepts(): array { $templateType = static fn ($name, ?Type $bound, ?string $functionName = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction($functionName ?? '_'), @@ -96,9 +97,7 @@ public function dataAccepts(): array ]; } - /** - * @dataProvider dataAccepts - */ + #[DataProvider('dataAccepts')] public function testAccepts( Type $type, Type $otherType, @@ -108,7 +107,7 @@ public function testAccepts( { assert($type instanceof TemplateType); - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedAccept->describe(), $actualResult->describe(), @@ -117,7 +116,7 @@ public function testAccepts( $type = $type->toArgument(); - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedAcceptArg->describe(), $actualResult->describe(), @@ -125,7 +124,7 @@ public function testAccepts( ); } - public function dataIsSuperTypeOf(): array + public static function dataIsSuperTypeOf(): array { $templateType = static fn ($name, ?Type $bound, ?string $functionName = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction($functionName ?? '_'), @@ -285,9 +284,7 @@ public function dataIsSuperTypeOf(): array ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf( Type $type, Type $otherType, @@ -313,7 +310,7 @@ public function testIsSuperTypeOf( } /** @return array}> */ - public function dataInferTemplateTypes(): array + public static function dataInferTemplateTypes(): array { $templateType = static fn ($name, ?Type $bound = null, ?string $functionName = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction($functionName ?? '_'), @@ -361,9 +358,9 @@ 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); diff --git a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php index 55f44ec4fe..822844c0e2 100644 --- a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php +++ b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php @@ -3,15 +3,14 @@ namespace PHPStan\Type; use PHPStan\Fixture\TestDecimal; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use stdClass; class TestDecimalOperatorTypeSpecifyingExtensionTest extends TestCase { - /** - * @dataProvider dataSigilAndSidesProvider - */ + #[DataProvider('dataSigilAndSidesProvider')] public function testSupportsMatchingSigilsAndSides(string $sigil, Type $leftType, Type $rightType): void { $extension = new TestDecimalOperatorTypeSpecifyingExtension(); @@ -21,7 +20,7 @@ public function testSupportsMatchingSigilsAndSides(string $sigil, Type $leftType self::assertTrue($result); } - public function dataSigilAndSidesProvider(): iterable + public static function dataSigilAndSidesProvider(): iterable { yield '+' => [ '+', @@ -60,9 +59,7 @@ public function dataSigilAndSidesProvider(): iterable ]; } - /** - * @dataProvider dataNotMatchingSidesProvider - */ + #[DataProvider('dataNotMatchingSidesProvider')] public function testNotSupportsNotMatchingSides(string $sigil, Type $leftType, Type $rightType): void { $extension = new TestDecimalOperatorTypeSpecifyingExtension(); @@ -72,7 +69,7 @@ public function testNotSupportsNotMatchingSides(string $sigil, Type $leftType, T self::assertFalse($result); } - public function dataNotMatchingSidesProvider(): iterable + public static function dataNotMatchingSidesProvider(): iterable { yield 'left' => [ '+', diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index e521c3edba..a989ec244a 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -15,13 +15,16 @@ 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; @@ -36,6 +39,7 @@ use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\GenericStaticType; use PHPStan\Type\Generic\TemplateBenevolentUnionType; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateObjectType; @@ -44,6 +48,7 @@ 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; @@ -53,6 +58,7 @@ use Traversable; use function array_map; use function array_reverse; +use function get_class; use function implode; use function sprintf; use const PHP_VERSION_ID; @@ -60,7 +66,7 @@ class TypeCombinatorTest extends PHPStanTestCase { - public function dataAddNull(): array + public static function dataAddNull(): array { return [ [ @@ -123,9 +129,9 @@ public function dataAddNull(): array } /** - * @dataProvider dataAddNull * @param class-string $expectedTypeClass */ + #[DataProvider('dataAddNull')] public function testAddNull( Type $type, string $expectedTypeClass, @@ -138,9 +144,9 @@ public function testAddNull( } /** - * @dataProvider dataAddNull * @param class-string $expectedTypeClass */ + #[DataProvider('dataAddNull')] public function testUnionWithNull( Type $type, string $expectedTypeClass, @@ -152,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 [ [ @@ -241,9 +247,9 @@ public function dataRemoveNull(): array } /** - * @dataProvider dataRemoveNull * @param class-string $expectedTypeClass */ + #[DataProvider('dataRemoveNull')] public function testRemoveNull( Type $type, string $expectedTypeClass, @@ -255,7 +261,7 @@ public function testRemoveNull( $this->assertInstanceOf($expectedTypeClass, $result); } - public function dataUnion(): iterable + public static function dataUnion(): iterable { yield from [ [ @@ -872,20 +878,6 @@ public function dataUnion(): iterable UnionType::class, "'bar'|'barr'|'baz'|'bazz'|'foo'|'fooo'|'lorem'|'loremm'|'loremmm'", ], - [ - [ - new IntersectionType([ - new ArrayType(new MixedType(), new StringType()), - new HasOffsetType(new StringType()), - ]), - new IntersectionType([ - new ArrayType(new MixedType(), new StringType()), - new HasOffsetType(new StringType()), - ]), - ], - IntersectionType::class, - 'array&hasOffset(string)', - ], [ [ new IntersectionType([ @@ -962,8 +954,8 @@ public function dataUnion(): iterable new HasOffsetType(new ConstantStringType('bar')), ]), ], - ArrayType::class, - 'array', + IntersectionType::class, + 'non-empty-array', ], [ [ @@ -978,7 +970,21 @@ public function dataUnion(): iterable ]), ], IntersectionType::class, - 'array&hasOffsetValue(\'foo\', mixed)', + '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', ], [ [ @@ -990,16 +996,16 @@ public function dataUnion(): iterable ], [ [ - 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(), ])), @@ -1009,8 +1015,8 @@ public function dataUnion(): iterable ], [ [ - 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(), ])), @@ -1020,8 +1026,8 @@ public function dataUnion(): iterable ], [ [ - 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(), ])), @@ -1031,8 +1037,8 @@ public function dataUnion(): iterable ], [ [ - 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', @@ -1040,14 +1046,14 @@ public function dataUnion(): iterable [ [ 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(), @@ -1074,15 +1080,15 @@ public function dataUnion(): iterable ], [ [ - 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, @@ -1090,7 +1096,7 @@ public function dataUnion(): iterable ], [ [ - new MixedType(false, new NullType()), + new MixedType(subtractedType: new NullType()), new NullType(), ], MixedType::class, @@ -1098,7 +1104,7 @@ public function dataUnion(): iterable ], [ [ - new MixedType(false, new IntegerType()), + new MixedType(subtractedType: new IntegerType()), new IntegerType(), ], MixedType::class, @@ -1106,7 +1112,7 @@ public function dataUnion(): iterable ], [ [ - new MixedType(false, new ConstantIntegerType(1)), + new MixedType(subtractedType: new ConstantIntegerType(1)), new ConstantIntegerType(1), ], MixedType::class, @@ -1114,7 +1120,7 @@ public function dataUnion(): iterable ], [ [ - new MixedType(false, new ObjectType('Exception')), + new MixedType(subtractedType: new ObjectType('Exception')), new ObjectType('Throwable'), ], MixedType::class, @@ -1122,7 +1128,7 @@ public function dataUnion(): iterable ], [ [ - new MixedType(false, new ObjectType('Exception')), + new MixedType(subtractedType: new ObjectType('Exception')), new ObjectType('Exception'), ], MixedType::class, @@ -1130,16 +1136,16 @@ public function dataUnion(): iterable ], [ [ - 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', @@ -1147,7 +1153,7 @@ public function dataUnion(): iterable [ [ new MixedType(), - new MixedType(false, new NullType()), + new MixedType(subtractedType: new NullType()), ], MixedType::class, 'mixed=implicit', @@ -1570,6 +1576,34 @@ public function dataUnion(): iterable UnionType::class, 'int<1, 3>|int<7, 9>', ], + [ + [ + IntegerRangeType::fromInterval(4, 9), + IntegerRangeType::fromInterval(16, 81), + IntegerRangeType::fromInterval(8, 27), + ], + IntegerRangeType::class, + 'int<4, 81>', + ], + [ + [ + IntegerRangeType::fromInterval(8, 27), + IntegerRangeType::fromInterval(4, 6), + new ConstantIntegerType(7), + IntegerRangeType::fromInterval(16, 81), + ], + IntegerRangeType::class, + 'int<4, 81>', + ], + [ + [ + new IntegerType(), + IntegerRangeType::fromInterval(null, -1), + IntegerRangeType::fromInterval(1, null), + ], + IntegerType::class, + 'int', + ], [ [ IntegerRangeType::fromInterval(1, 3), @@ -1769,7 +1803,7 @@ public function dataUnion(): iterable [ [ new StringType(), - new MixedType(false, new StringType()), + new MixedType(subtractedType: new StringType()), ], MixedType::class, 'mixed=implicit', @@ -1793,7 +1827,7 @@ public function dataUnion(): iterable new ConstantIntegerType(0), ], [ new StringType(), - ], 1, [0]), + ], [1], [0]), ], UnionType::class, 'array{}|array{0?: string}', @@ -1856,22 +1890,6 @@ public function dataUnion(): iterable UnionType::class, 'array{a: int, b: int}|array{b: int, c: int}', ], - [ - [ - TypeCombinator::intersect(new StringType(), new HasOffsetType(new IntegerType())), - TypeCombinator::intersect(new StringType(), new HasOffsetType(new IntegerType())), - ], - IntersectionType::class, - 'string&hasOffset(int)', - ], - [ - [ - TypeCombinator::intersect(new ConstantStringType('abc'), new HasOffsetType(new IntegerType())), - TypeCombinator::intersect(new ConstantStringType('abc'), new HasOffsetType(new IntegerType())), - ], - IntersectionType::class, - '\'abc\'&hasOffset(int)', - ], [ [ StaticTypeFactory::falsey(), @@ -1886,7 +1904,7 @@ public function dataUnion(): iterable StaticTypeFactory::truthy(), ], MixedType::class, - 'mixed~0|0.0|\'\'|\'0\'|array{}|false|null=implicit', + 'mixed~(0|0.0|\'\'|\'0\'|array{}|false|null)=implicit', ], [ [ @@ -1960,6 +1978,102 @@ public function dataUnion(): iterable UnionType::class, 'string|false', ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + ], + UnionType::class, + 'literal-string|numeric-string', + ], + [ + [ + new StringType(), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + StringType::class, + 'string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|numeric-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|non-falsy-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|non-empty-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'literal-string|lowercase-string', + ], + [ + [ + new StringType(), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + StringType::class, + 'string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'numeric-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'non-falsy-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'non-empty-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'literal-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|uppercase-string', + ], [ [ TemplateTypeFactory::create( @@ -2107,14 +2221,14 @@ public function dataUnion(): iterable ]; yield [ [ - new MixedType(false, IntegerRangeType::fromInterval(17, null)), + new MixedType(subtractedType: IntegerRangeType::fromInterval(17, null)), IntegerRangeType::fromInterval(19, null), ], MixedType::class, 'mixed~int<17, 18>=implicit', ]; - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); yield [ [ new StaticType($reflectionProvider->getClass(stdClass::class)), @@ -2163,6 +2277,36 @@ public function dataUnion(): iterable '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( @@ -2248,7 +2392,7 @@ public function dataUnion(): iterable TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), new HasOffsetValueType(new ConstantStringType('a'), new IntegerType())), ], IntersectionType::class, - 'array&hasOffsetValue(\'a\', int)', + 'non-empty-array&hasOffsetValue(\'a\', int)', ]; yield [ @@ -2269,7 +2413,7 @@ public function dataUnion(): iterable ]), ], IntersectionType::class, - "array&hasOffsetValue('a', mixed)", + "non-empty-array&hasOffsetValue('a', mixed)", ]; yield [ @@ -2296,7 +2440,7 @@ public function dataUnion(): iterable ]), ], IntersectionType::class, - "array&hasOffsetValue(0, array&hasOffsetValue('code', mixed))", + "non-empty-array&hasOffsetValue(0, non-empty-array&hasOffsetValue('code', mixed))", ]; yield [ @@ -2304,14 +2448,12 @@ public function dataUnion(): iterable new ConstantArrayType( [new ConstantStringType('default'), new ConstantStringType('range')], [new ObjectType(Foo::class), new ObjectType(Foo::class)], - [0], - [0, 1], + optionalKeys: [0, 1], ), new ConstantArrayType( [new ConstantStringType('range')], [new ObjectType(Foo::class)], - [0], - [0], + optionalKeys: [0], ), ], ConstantArrayType::class, @@ -2324,16 +2466,14 @@ public function dataUnion(): iterable new ConstantArrayType( [new ConstantStringType('default'), new ConstantStringType('range')], [new ObjectType(Foo::class), new ObjectType(Foo::class)], - [0], - [0, 1], + optionalKeys: [0, 1], ), new NonEmptyArrayType(), ]), new ConstantArrayType( [new ConstantStringType('range')], [new ObjectType(Foo::class)], - [0], - [0], + optionalKeys: [0], ), ], ConstantArrayType::class, @@ -2460,14 +2600,14 @@ public function dataUnion(): iterable ], [ new ConstantBooleanType(true), new ConstantBooleanType(true), - ], [0], [0]), + ], optionalKeys: [0]), new ConstantArrayType([ new ConstantStringType('a'), new ConstantStringType('c'), ], [ new ConstantBooleanType(true), new ConstantBooleanType(true), - ], [0], [0, 1]), + ], optionalKeys: [0, 1]), ], UnionType::class, 'array{a?: true, b: true}|array{a?: true, c?: true}', @@ -2481,7 +2621,7 @@ public function dataUnion(): iterable ], [ new ConstantBooleanType(true), new ConstantBooleanType(true), - ], [0], [0]), + ], optionalKeys: [0]), new IntersectionType([ new ConstantArrayType([ new ConstantStringType('a'), @@ -2489,12 +2629,12 @@ public function dataUnion(): iterable ], [ new ConstantBooleanType(true), new ConstantBooleanType(true), - ], [0], [0, 1]), + ], optionalKeys: [0, 1]), new NonEmptyArrayType(), ]), ], UnionType::class, - 'array{a?: true, b: true}|(array{a?: true, c?: true}&non-empty-array)', + 'array{a?: true, b: true}|non-empty-array{a?: true, c?: true}', ]; yield [ @@ -2526,7 +2666,7 @@ public function dataUnion(): iterable ]; yield [ [ - new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(isPure: TrinaryLogic::createYes()), new CallableType(), ], CallableType::class, @@ -2534,7 +2674,7 @@ public function dataUnion(): iterable ]; yield [ [ - new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(isPure: TrinaryLogic::createYes()), ClosureType::createPure(), ], CallableType::class, @@ -2542,15 +2682,15 @@ public function dataUnion(): iterable ]; yield [ [ - new CallableType(null, null, true, null, null, [], TrinaryLogic::createMaybe()), - new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(isPure: TrinaryLogic::createMaybe()), + new CallableType(isPure: TrinaryLogic::createYes()), ], CallableType::class, 'callable(): mixed', ]; yield [ [ - new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new ClosureType([], new MixedType(), impurePoints: [ new SimpleImpurePoint('functionCall', 'foo', true), ]), ClosureType::createPure(), @@ -2560,7 +2700,7 @@ public function dataUnion(): iterable ]; yield [ [ - new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new ClosureType([], new MixedType(), impurePoints: [ new SimpleImpurePoint('functionCall', 'foo', false), ]), ClosureType::createPure(), @@ -2581,15 +2721,86 @@ public function dataUnion(): iterable ]), ], IntersectionType::class, - 'array&hasOffsetValue(\'thing\', mixed)', + '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(), ]; } /** - * @dataProvider dataUnion * @param Type[] $types * @param class-string $expectedTypeClass */ + #[DataProvider('dataUnion')] public function testUnion( array $types, string $expectedTypeClass, @@ -2605,6 +2816,16 @@ 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, @@ -2632,10 +2853,10 @@ public function testUnion( } /** - * @dataProvider dataUnion * @param Type[] $types * @param class-string $expectedTypeClass */ + #[DataProvider('dataUnion')] public function testUnionInversed( array $types, string $expectedTypeClass, @@ -2652,6 +2873,16 @@ 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, @@ -2663,9 +2894,9 @@ public function testUnionInversed( $this->assertInstanceOf($expectedTypeClass, $actualType); } - public function dataIntersect(): iterable + public static function dataIntersect(): iterable { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); yield from [ [ @@ -3090,7 +3321,7 @@ public function dataIntersect(): iterable new HasOffsetType(new ConstantStringType('a')), ], IntersectionType::class, - 'array&hasOffset(\'a\')', + 'non-empty-array&hasOffset(\'a\')', ], [ [ @@ -3099,25 +3330,7 @@ public function dataIntersect(): iterable new HasOffsetType(new ConstantStringType('a')), ], IntersectionType::class, - 'array&hasOffset(\'a\')', - ], - [ - [ - new ArrayType(new StringType(), new StringType()), - new HasOffsetType(new StringType()), - new HasOffsetType(new StringType()), - ], - IntersectionType::class, - 'array&hasOffset(string)', - ], - [ - [ - new ArrayType(new MixedType(), new MixedType()), - new HasOffsetType(new StringType()), - new HasOffsetType(new StringType()), - ], - IntersectionType::class, - 'array&hasOffset(string)', + 'non-empty-array&hasOffset(\'a\')', ], [ [ @@ -3204,17 +3417,6 @@ public function dataIntersect(): iterable ClosureType::class, 'Closure(): mixed', ], - [ - [ - new UnionType([ - new ArrayType(new MixedType(), new StringType()), - new NullType(), - ]), - new HasOffsetType(new StringType()), - ], - IntersectionType::class, - 'array&hasOffset(string)', - ], [ [ new ArrayType(new MixedType(), new MixedType()), @@ -3277,7 +3479,7 @@ public function dataIntersect(): iterable ]), ], IntersectionType::class, - 'array&hasOffset(\'bar\')&hasOffset(\'foo\')', + 'non-empty-array&hasOffset(\'bar\')&hasOffset(\'foo\')', ], [ [ @@ -3289,7 +3491,7 @@ public function dataIntersect(): iterable ], [ [ - new MixedType(false, new StringType()), + new MixedType(subtractedType: new StringType()), new StringType(), ], NeverType::class, @@ -3297,7 +3499,7 @@ public function dataIntersect(): iterable ], [ [ - new MixedType(false, new StringType()), + new MixedType(subtractedType: new StringType()), new ConstantStringType('foo'), ], NeverType::class, @@ -3305,7 +3507,7 @@ public function dataIntersect(): iterable ], [ [ - new MixedType(false, new StringType()), + new MixedType(subtractedType: new StringType()), new ConstantIntegerType(1), ], ConstantIntegerType::class, @@ -3313,11 +3515,11 @@ public function dataIntersect(): iterable ], [ [ - new MixedType(false, new StringType()), - new MixedType(false, new IntegerType()), + new MixedType(subtractedType: new StringType()), + new MixedType(subtractedType: new IntegerType()), ], MixedType::class, - 'mixed~int|string=implicit', + 'mixed~(int|string)=implicit', ], [ [ @@ -3717,20 +3919,12 @@ public function dataIntersect(): iterable ], [ new IntegerType(), new IntegerType(), - ], 2, [0]), + ], [2], [0]), new HasOffsetType(new ConstantStringType('a')), ], ConstantArrayType::class, 'array{a: int, b: int}', ], - [ - [ - new StringType(), - new HasOffsetType(new IntegerType()), - ], - IntersectionType::class, - 'string&hasOffset(int)', - ], [ [ new BenevolentUnionType([new IntegerType(), new StringType()]), @@ -3845,6 +4039,110 @@ public function dataIntersect(): iterable NeverType::class, '*NEVER*=implicit', ], + [ + [ + new StringType(), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + IntersectionType::class, + 'lowercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + IntersectionType::class, + 'lowercase-string&numeric-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + IntersectionType::class, + 'lowercase-string&non-falsy-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + IntersectionType::class, + 'lowercase-string&non-empty-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + IntersectionType::class, + 'literal-string&lowercase-string', + ], + [ + [ + new StringType(), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'numeric-string&uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'non-falsy-string&uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'non-empty-string&uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'literal-string&uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'lowercase-string&uppercase-string', + ], + [ + [ + new ObjectType(DateTime::class), + new MixedType(subtractedType: new NullType()), + ], + ObjectType::class, + 'DateTime', + ], + [ + [ + new ObjectWithoutClassType(), + new MixedType(subtractedType: new NullType()), + ], + ObjectWithoutClassType::class, + 'object', + ], ]; if (PHP_VERSION_ID < 80100) { @@ -3941,7 +4239,7 @@ public function dataIntersect(): iterable ]; yield [ [ - new MixedType(false, IntegerRangeType::fromInterval(17, null)), + new MixedType(subtractedType: IntegerRangeType::fromInterval(17, null)), new MixedType(), ], MixedType::class, @@ -3983,6 +4281,27 @@ public function dataIntersect(): iterable '$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()), @@ -4025,7 +4344,7 @@ public function dataIntersect(): iterable TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), new HasOffsetValueType(new ConstantStringType('a'), new IntegerType())), ], IntersectionType::class, - 'array&hasOffsetValue(\'a\', 1)', + 'non-empty-array&hasOffsetValue(\'a\', 1)', ]; yield [ [ @@ -4173,19 +4492,19 @@ public function dataIntersect(): iterable ], [ new ConstantBooleanType(true), new ConstantBooleanType(true), - ], [0], [0]), + ], optionalKeys: [0]), new ConstantArrayType([ new ConstantStringType('a'), new ConstantStringType('c'), ], [ new ConstantBooleanType(true), new ConstantBooleanType(true), - ], [0], [0, 1]), + ], optionalKeys: [0, 1]), ]), new NonEmptyArrayType(), ], UnionType::class, - 'array{a?: true, b: true}|(array{a?: true, c?: true}&non-empty-array)', + 'array{a?: true, b: true}|non-empty-array{a?: true, c?: true}', ]; yield [ [ @@ -4203,7 +4522,7 @@ public function dataIntersect(): iterable ], [ new ConstantBooleanType(true), new ConstantBooleanType(true), - ], [0], [0]), + ], optionalKeys: [0]), new NonEmptyArrayType(), ], ConstantArrayType::class, @@ -4217,15 +4536,15 @@ public function dataIntersect(): iterable ], [ new ConstantBooleanType(true), new ConstantBooleanType(true), - ], [0], [0, 1]), + ], optionalKeys: [0, 1]), new NonEmptyArrayType(), ], IntersectionType::class, - 'array{a?: true, c?: true}&non-empty-array', + 'non-empty-array{a?: true, c?: true}', ]; yield [ [ - new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(isPure: TrinaryLogic::createYes()), new CallableType(), ], CallableType::class, @@ -4233,7 +4552,7 @@ public function dataIntersect(): iterable ]; yield [ [ - new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(isPure: TrinaryLogic::createYes()), ClosureType::createPure(), ], ClosureType::class, @@ -4241,15 +4560,15 @@ public function dataIntersect(): iterable ]; yield [ [ - new CallableType(null, null, true, null, null, [], TrinaryLogic::createMaybe()), - new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(isPure: TrinaryLogic::createMaybe()), + new CallableType(isPure: TrinaryLogic::createYes()), ], CallableType::class, 'pure-callable(): mixed', ]; yield [ [ - new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new ClosureType([], new MixedType(), impurePoints: [ new SimpleImpurePoint('functionCall', 'foo', true), ]), ClosureType::createPure(), @@ -4259,7 +4578,7 @@ public function dataIntersect(): iterable ]; yield [ [ - new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new ClosureType([], new MixedType(), impurePoints: [ new SimpleImpurePoint('functionCall', 'foo', false), ]), ClosureType::createPure(), @@ -4317,13 +4636,118 @@ public function dataIntersect(): iterable NeverType::class, '*NEVER*=implicit', ]; + + yield [ + [ + new ConstantStringType('FOO'), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ConstantStringType('foo'), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + ConstantStringType::class, + '\'foo\'', + ]; + + yield [ + [ + new ConstantStringType('foo'), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ConstantStringType('FOO'), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + ConstantStringType::class, + '\'FOO\'', + ]; + + $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()]), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + + 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()]), + ], + GenericStaticType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)', + ]; + + yield [ + [ + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), + new ObjectWithoutClassType(), + ], + GenericStaticType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)', + ]; + + yield [ + [ + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), + new ObjectType($c->getName()), + ], + GenericStaticType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)', + ]; + + $nonFinalClass = $reflectionProvider->getClass(\NullCoalesceIsAlwaysFinal\Foo::class); + $finalClass = $nonFinalClass->asFinal(); + + yield [ + [ + new ObjectType($finalClass->getName(), classReflection: $finalClass), + new ObjectType($nonFinalClass->getName(), classReflection: $nonFinalClass), + ], + ObjectType::class, + $nonFinalClass->getDisplayName() . '=final', + ]; } /** - * @dataProvider dataIntersect * @param Type[] $types * @param class-string $expectedTypeClass */ + #[DataProvider('dataIntersect')] public function testIntersect( array $types, string $expectedTypeClass, @@ -4346,15 +4770,27 @@ public function testIntersect( $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 Type[] $types * @param class-string $expectedTypeClass */ + #[DataProvider('dataIntersect')] public function testIntersectInversed( array $types, string $expectedTypeClass, @@ -4377,11 +4813,22 @@ public function testIntersectInversed( $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 [ [ @@ -4486,7 +4933,7 @@ public function dataRemove(): array StaticTypeFactory::truthy(), StaticTypeFactory::falsey(), MixedType::class, - 'mixed~0|0.0|\'\'|\'0\'|array{}|false|null', + 'mixed~(0|0.0|\'\'|\'0\'|array{}|false|null)', ], [ StaticTypeFactory::falsey(), @@ -4653,16 +5100,16 @@ 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), @@ -4671,19 +5118,19 @@ public function dataRemove(): array '*NEVER*=implicit', ], [ - new MixedType(false, new StringType()), + new MixedType(subtractedType: new StringType()), new MixedType(), NeverType::class, '*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', @@ -4698,7 +5145,7 @@ public function dataRemove(): array new ObjectType('Exception', new ObjectType('InvalidArgumentException')), new ObjectType('LengthException'), ObjectType::class, - 'Exception~InvalidArgumentException|LengthException', + 'Exception~(InvalidArgumentException|LengthException)', ], [ new ObjectType('Exception'), @@ -4794,7 +5241,7 @@ public function dataRemove(): array ], [ new StringType(), new StringType(), - ], 2), + ], [2]), new HasOffsetType(new ConstantIntegerType(1)), NeverType::class, '*NEVER*=implicit', @@ -4806,7 +5253,7 @@ public function dataRemove(): array ], [ new StringType(), new StringType(), - ], 2, [1]), + ], [2], [1]), new HasOffsetType(new ConstantIntegerType(1)), ConstantArrayType::class, 'array{string}', @@ -4818,7 +5265,7 @@ public function dataRemove(): array ], [ new StringType(), new StringType(), - ], 2, [1]), + ], [2], [1]), new HasOffsetType(new ConstantIntegerType(0)), NeverType::class, '*NEVER*=implicit', @@ -4880,9 +5327,9 @@ public function dataRemove(): array } /** - * @dataProvider dataRemove * @param class-string $expectedTypeClass */ + #[DataProvider('dataRemove')] public function testRemove( Type $fromType, Type $type, @@ -4922,9 +5369,7 @@ public function testSpecificUnionConstantArray(): void $this->assertSame('array{0: string, 1?: string, 2?: string, 3?: string, 4?: string, test?: string}', $resultType->describe(VerbosityLevel::precise())); } - /** - * @dataProvider dataContainsNull - */ + #[DataProvider('dataContainsNull')] public function testContainsNull( Type $type, bool $expectedResult, @@ -4933,7 +5378,7 @@ public function testContainsNull( $this->assertSame($expectedResult, TypeCombinator::containsNull($type)); } - public function dataContainsNull(): iterable + public static function dataContainsNull(): iterable { yield [new NullType(), true]; yield [new UnionType([new IntegerType(), new NullType()]), true]; diff --git a/tests/PHPStan/Type/TypeGetFiniteTypesTest.php b/tests/PHPStan/Type/TypeGetFiniteTypesTest.php index ce605f3d5a..e7e0ae2de6 100644 --- a/tests/PHPStan/Type/TypeGetFiniteTypesTest.php +++ b/tests/PHPStan/Type/TypeGetFiniteTypesTest.php @@ -3,17 +3,19 @@ namespace PHPStan\Type; 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\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPUnit\Framework\Attributes\DataProvider; class TypeGetFiniteTypesTest extends PHPStanTestCase { - public function dataGetFiniteTypes(): iterable + public static function dataGetFiniteTypes(): iterable { yield [ IntegerRangeType::fromInterval(0, 5), @@ -88,7 +90,7 @@ public function dataGetFiniteTypes(): iterable ], [ new BooleanType(), new BooleanType(), - ], 2), + ], [2]), [ new ConstantArrayType([ new ConstantIntegerType(0), @@ -96,36 +98,36 @@ public function dataGetFiniteTypes(): iterable ], [ new ConstantBooleanType(true), new ConstantBooleanType(true), - ], 2, [], true), + ], [2], isList: TrinaryLogic::createYes()), new ConstantArrayType([ new ConstantIntegerType(0), new ConstantIntegerType(1), ], [ new ConstantBooleanType(true), new ConstantBooleanType(false), - ], 2, [], true), + ], [2], isList: TrinaryLogic::createYes()), new ConstantArrayType([ new ConstantIntegerType(0), new ConstantIntegerType(1), ], [ new ConstantBooleanType(false), new ConstantBooleanType(true), - ], 2, [], true), + ], [2], isList: TrinaryLogic::createYes()), new ConstantArrayType([ new ConstantIntegerType(0), new ConstantIntegerType(1), ], [ new ConstantBooleanType(false), new ConstantBooleanType(false), - ], 2, [], true), + ], [2], isList: TrinaryLogic::createYes()), ], ]; } /** - * @dataProvider dataGetFiniteTypes * @param list $expectedTypes */ + #[DataProvider('dataGetFiniteTypes')] public function testGetFiniteTypes( Type $type, array $expectedTypes, diff --git a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php index 592271bdda..29bfe8f70a 100644 --- a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php +++ b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php @@ -4,10 +4,13 @@ use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Testing\PHPStanTestCase; +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\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -17,6 +20,7 @@ use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPUnit\Framework\Attributes\DataProvider; use stdClass; use function sprintf; use const PHP_INT_MAX; @@ -25,7 +29,7 @@ class TypeToPhpDocNodeTest extends PHPStanTestCase { - public function dataToPhpDocNode(): iterable + public static function dataToPhpDocNode(): iterable { yield [ new ArrayType(new MixedType(), new MixedType()), @@ -63,7 +67,7 @@ public function dataToPhpDocNode(): iterable new ConstantIntegerType(2), new ConstantIntegerType(3), new ConstantIntegerType(4), - ], [0], [2]), + ], optionalKeys: [2]), 'array{foo: 1, bar: 2, baz?: 3, \'$ref\': 4}', ]; @@ -101,7 +105,7 @@ public function dataToPhpDocNode(): iterable new ConstantStringType('foo'), new ConstantStringType('bar'), new ConstantStringType('baz'), - ], [0], [2]), + ], optionalKeys: [2]), 'array{1: \'foo\', 2: \'bar\', 3?: \'baz\'}', ]; @@ -148,7 +152,7 @@ public function dataToPhpDocNode(): iterable new StringType(), new IntegerType(), new MixedType(), - ], null, null, [ + ], variances: [ TemplateTypeVariance::createInvariant(), TemplateTypeVariance::createContravariant(), TemplateTypeVariance::createBivariant(), @@ -217,6 +221,16 @@ public function dataToPhpDocNode(): iterable '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', @@ -259,7 +273,7 @@ public function dataToPhpDocNode(): iterable yield [ new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType()), new AccessoryArrayListType()]), - 'list', + 'list', ]; yield [ @@ -268,7 +282,7 @@ public function dataToPhpDocNode(): iterable new NonEmptyArrayType(), new AccessoryArrayListType(), ]), - 'non-empty-list', + 'non-empty-list', ]; yield [ @@ -303,11 +317,125 @@ public function dataToPhpDocNode(): iterable ], [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 - */ + #[DataProvider('dataToPhpDocNode')] public function testToPhpDocNode(Type $type, string $expected): void { $phpDocNode = $type->toPhpDocNode(); @@ -320,11 +448,16 @@ public function testToPhpDocNode(Type $type, string $expected): void $this->assertTrue($type->equals($parsedType), sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $parsedType->describe(VerbosityLevel::precise()))); } - public function dataToPhpDocNodeWithoutCheckingEquals(): iterable + public static function dataToPhpDocNodeWithoutCheckingEquals(): iterable { yield [ new ConstantStringType("foo\nbar\nbaz"), - '(literal-string & non-falsy-string)', + '(literal-string & lowercase-string & non-falsy-string)', + ]; + + yield [ + new ConstantStringType("FOO\nBAR\nBAZ"), + '(literal-string & non-falsy-string & uppercase-string)', ]; yield [ @@ -383,9 +516,7 @@ public function dataToPhpDocNodeWithoutCheckingEquals(): iterable ]; } - /** - * @dataProvider dataToPhpDocNodeWithoutCheckingEquals - */ + #[DataProvider('dataToPhpDocNodeWithoutCheckingEquals')] public function testToPhpDocNodeWithoutCheckingEquals(Type $type, string $expected): void { $phpDocNode = $type->toPhpDocNode(); @@ -397,9 +528,9 @@ public function testToPhpDocNodeWithoutCheckingEquals(Type $type, string $expect $typeStringResolver->resolve($typeString); } - public function dataFromTypeStringToPhpDocNode(): iterable + public static function dataFromTypeStringToPhpDocNode(): iterable { - foreach ($this->dataToPhpDocNode() as [, $typeString]) { + foreach (self::dataToPhpDocNode() as [, $typeString]) { yield [$typeString]; } @@ -414,9 +545,7 @@ public function dataFromTypeStringToPhpDocNode(): iterable yield ['Closure(Foo $foo=, Bar $bar=): (Closure(Foo): Bar)']; } - /** - * @dataProvider dataFromTypeStringToPhpDocNode - */ + #[DataProvider('dataFromTypeStringToPhpDocNode')] public function testFromTypeStringToPhpDocNode(string $typeString): void { $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); diff --git a/tests/PHPStan/Type/UnionTypeTest.php b/tests/PHPStan/Type/UnionTypeTest.php index cd0ee180cc..c93a5e8d47 100644 --- a/tests/PHPStan/Type/UnionTypeTest.php +++ b/tests/PHPStan/Type/UnionTypeTest.php @@ -27,6 +27,7 @@ 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 function array_merge; @@ -38,7 +39,7 @@ class UnionTypeTest extends PHPStanTestCase { - public function dataIsCallable(): array + public static function dataIsCallable(): array { return [ [ @@ -75,9 +76,7 @@ public function dataIsCallable(): array ]; } - /** - * @dataProvider dataIsCallable - */ + #[DataProvider('dataIsCallable')] public function testIsCallable(UnionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isCallable(); @@ -88,9 +87,9 @@ public function testIsCallable(UnionType $type, TrinaryLogic $expectedResult): v ); } - public function dataSelfCompare(): Iterator + public static function dataSelfCompare(): Iterator { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $integerType = new IntegerType(); $stringType = new StringType(); @@ -111,7 +110,7 @@ public function dataSelfCompare(): Iterator yield [new CallableType([$mixedParam, $integerParam], $stringType, false)]; yield [new ClassStringType()]; yield [new ClosureType([$mixedParam, $integerParam], $stringType, false)]; - yield [new ConstantArrayType([$constantStringType, $constantIntegerType], [$mixedType, $stringType], 10, [1])]; + yield [new ConstantArrayType([$constantStringType, $constantIntegerType], [$mixedType, $stringType], [10], [1])]; yield [new ConstantBooleanType(true)]; yield [new ConstantFloatType(3.14)]; yield [$constantIntegerType]; @@ -147,10 +146,7 @@ public function dataSelfCompare(): Iterator yield [new VoidType()]; } - /** - * @dataProvider dataSelfCompare - * - */ + #[DataProvider('dataSelfCompare')] public function testSelfCompare(Type $type): void { $description = $type->describe(VerbosityLevel::precise()); @@ -158,7 +154,7 @@ public function testSelfCompare(Type $type): void $type->equals($type), sprintf('%s -> equals(itself)', $description), ); - $this->assertEquals( + $this->assertSame( 'Yes', $type->isSuperTypeOf($type)->describe(), sprintf('%s -> isSuperTypeOf(itself)', $description), @@ -170,7 +166,7 @@ public function testSelfCompare(Type $type): void ); } - public function dataIsSuperTypeOf(): Iterator + public static function dataIsSuperTypeOf(): Iterator { $unionTypeA = new UnionType([ new IntegerType(), @@ -453,9 +449,7 @@ public function dataIsSuperTypeOf(): Iterator ]; } - /** - * @dataProvider dataIsSuperTypeOf - */ + #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf(UnionType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -466,7 +460,7 @@ public function testIsSuperTypeOf(UnionType $type, Type $otherType, TrinaryLogic ); } - public function dataIsSubTypeOf(): Iterator + public static function dataIsSubTypeOf(): Iterator { $unionTypeA = new UnionType([ new IntegerType(), @@ -621,9 +615,7 @@ public function dataIsSubTypeOf(): Iterator ]; } - /** - * @dataProvider dataIsSubTypeOf - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOf(UnionType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSubTypeOf($otherType); @@ -634,9 +626,7 @@ public function testIsSubTypeOf(UnionType $type, Type $otherType, TrinaryLogic $ ); } - /** - * @dataProvider dataIsSubTypeOf - */ + #[DataProvider('dataIsSubTypeOf')] public function testIsSubTypeOfInversed(UnionType $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $otherType->isSuperTypeOf($type); @@ -647,7 +637,7 @@ public function testIsSubTypeOfInversed(UnionType $type, Type $otherType, Trinar ); } - public function dataIsScalar(): array + public static function dataIsScalar(): array { return [ [ @@ -699,7 +689,7 @@ public function dataIsScalar(): array ]; } - /** @dataProvider dataIsScalar */ + #[DataProvider('dataIsScalar')] public function testIsScalar(UnionType $type, TrinaryLogic $expectedResult): void { $actualResult = $type->isScalar(); @@ -710,7 +700,7 @@ public function testIsScalar(UnionType $type, TrinaryLogic $expectedResult): voi ); } - public function dataDescribe(): array + public static function dataDescribe(): array { return [ [ @@ -718,12 +708,14 @@ public function dataDescribe(): array '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([ @@ -743,6 +735,7 @@ public function dataDescribe(): array 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', @@ -767,6 +760,7 @@ public function dataDescribe(): array ), '\'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', ], [ @@ -789,6 +783,7 @@ public function dataDescribe(): array ), '\'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', ], [ @@ -811,6 +806,7 @@ public function dataDescribe(): array ), '\'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', ], [ @@ -832,6 +828,7 @@ public function dataDescribe(): array ), 'array{int, bool, float}|array{string}', 'array{int, bool, float}|array{string}', + 'array{int, bool, float}|array{string}', 'array', ], [ @@ -845,6 +842,7 @@ public function dataDescribe(): array ), 'array{}|array{foooo: \'barrr\'}', 'array{}|array{foooo: \'barrr\'}', + 'array{}|array{foooo: \'barrr\'}', 'array', ], [ @@ -857,6 +855,7 @@ public function dataDescribe(): array ), 'int|numeric-string', 'int|numeric-string', + 'int|numeric-string', 'int|string', ], [ @@ -867,6 +866,7 @@ public function dataDescribe(): array 'int<0, 4>|int<6, 10>', 'int<0, 4>|int<6, 10>', 'int<0, 4>|int<6, 10>', + 'int<0, 4>|int<6, 10>', ], [ TypeCombinator::union( @@ -879,6 +879,7 @@ public function dataDescribe(): array new NullType(), ), 'TFoo of int (class foo, parameter)|null', + 'TFoo of int (class foo, parameter)|null', '(TFoo of int)|null', '(TFoo of int)|null', ], @@ -892,6 +893,7 @@ public function dataDescribe(): array ), 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', @@ -907,6 +909,7 @@ public function dataDescribe(): array new NullType(), ), 'TFoo (class foo, parameter)|null', + 'TFoo (class foo, parameter)|null', 'TFoo|null', 'TFoo|null', ], @@ -926,11 +929,13 @@ public function dataDescribe(): array 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', @@ -938,22 +943,22 @@ public function dataDescribe(): array ]; } - /** - * @dataProvider dataDescribe - */ + #[DataProvider('dataDescribe')] public function testDescribe( Type $type, + string $expectedCacheDescription, string $expectedPreciseDescription, string $expectedValueDescription, string $expectedTypeOnlyDescription, ): void { + $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(): iterable + public static function dataAccepts(): iterable { yield from [ [ @@ -1294,9 +1299,7 @@ public function dataAccepts(): iterable ]; } - /** - * @dataProvider dataAccepts - */ + #[DataProvider('dataAccepts')] public function testAccepts( UnionType $type, Type $acceptedType, @@ -1305,12 +1308,12 @@ public function testAccepts( { $this->assertSame( $expectedResult->describe(), - $type->accepts($acceptedType, true)->describe(), + $type->accepts($acceptedType, true)->result->describe(), sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())), ); } - public function dataHasMethod(): array + public static function dataHasMethod(): array { return [ [ @@ -1336,9 +1339,7 @@ public function dataHasMethod(): array ]; } - /** - * @dataProvider dataHasMethod - */ + #[DataProvider('dataHasMethod')] public function testHasMethod( UnionType $type, string $methodName, @@ -1389,10 +1390,10 @@ public function testSorting(): void } /** - * @dataProvider dataGetConstantArrays * @param Type[] $types * @param list $expectedDescriptions */ + #[DataProvider('dataGetConstantArrays')] public function testGetConstantArrays( array $types, array $expectedDescriptions, @@ -1409,7 +1410,7 @@ public function testGetConstantArrays( $this->assertSame($expectedDescriptions, $actualDescriptions); } - public function dataGetConstantArrays(): iterable + public static function dataGetConstantArrays(): iterable { yield from [ [ @@ -1418,7 +1419,7 @@ public function dataGetConstantArrays(): iterable new ConstantArrayType( [new ConstantIntegerType(1), new ConstantIntegerType(2)], [new IntegerType(), new StringType()], - 2, + [2], [0, 1], ), new NonEmptyArrayType(), @@ -1426,7 +1427,7 @@ public function dataGetConstantArrays(): iterable new ConstantArrayType( [new ConstantIntegerType(0), new ConstantIntegerType(1)], [new ObjectType(Foo::class), new ObjectType(stdClass::class)], - 2, + [2], ), ], [ @@ -1440,7 +1441,7 @@ public function dataGetConstantArrays(): iterable new ConstantArrayType( [new ConstantIntegerType(1), new ConstantIntegerType(2)], [new IntegerType(), new StringType()], - 2, + [2], [0, 1], ), ), @@ -1452,9 +1453,9 @@ public function dataGetConstantArrays(): iterable } /** - * @dataProvider dataGetConstantStrings * @param list $expectedDescriptions */ + #[DataProvider('dataGetConstantStrings')] public function testGetConstantStrings( Type $unionType, array $expectedDescriptions, @@ -1470,7 +1471,7 @@ public function testGetConstantStrings( $this->assertSame($expectedDescriptions, $actualDescriptions); } - public function dataGetConstantStrings(): iterable + public static function dataGetConstantStrings(): iterable { yield from [ [ @@ -1526,9 +1527,9 @@ public function dataGetConstantStrings(): iterable } /** - * @dataProvider dataGetObjectClassNames * @param list $expectedObjectClassNames */ + #[DataProvider('dataGetObjectClassNames')] public function testGetObjectClassNames( Type $unionType, array $expectedObjectClassNames, @@ -1537,7 +1538,7 @@ public function testGetObjectClassNames( $this->assertSame($expectedObjectClassNames, $unionType->getObjectClassNames()); } - public function dataGetObjectClassNames(): iterable + public static function dataGetObjectClassNames(): iterable { yield from [ [ @@ -1568,9 +1569,9 @@ public function dataGetObjectClassNames(): iterable } /** - * @dataProvider dataGetArrays * @param list $expectedDescriptions */ + #[DataProvider('dataGetArrays')] public function testGetArrays( Type $unionType, array $expectedDescriptions, @@ -1586,7 +1587,7 @@ public function testGetArrays( $this->assertSame($expectedDescriptions, $actualDescriptions); } - public function dataGetArrays(): iterable + public static function dataGetArrays(): iterable { yield from [ [ @@ -1602,7 +1603,7 @@ public function dataGetArrays(): iterable new ConstantArrayType( [new ConstantIntegerType(1), new ConstantIntegerType(2)], [new IntegerType(), new StringType()], - 2, + [2], [0, 1], ), new NonEmptyArrayType(), @@ -1610,7 +1611,7 @@ public function dataGetArrays(): iterable new ConstantArrayType( [new ConstantIntegerType(0), new ConstantIntegerType(1)], [new ObjectType(Foo::class), new ObjectType(stdClass::class)], - 2, + [2], ), ), [ @@ -1624,13 +1625,13 @@ public function dataGetArrays(): iterable new ConstantArrayType( [new ConstantIntegerType(1), new ConstantIntegerType(2)], [new IntegerType(), new StringType()], - 2, + [2], [0, 1], ), new ConstantArrayType( [new ConstantIntegerType(0), new ConstantIntegerType(1)], [new ObjectType(Foo::class), new ObjectType(stdClass::class)], - 2, + [2], ), ), [ 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/composer.json b/tests/composer.json new file mode 100644 index 0000000000..aaad8c1afd --- /dev/null +++ b/tests/composer.json @@ -0,0 +1,12 @@ +{ + "name": "phpstan/phpstan-src-tests", + "require-dev": { + "phpunit/phpunit": "^11.5.23", + "brianium/paratest": "^7.8.3" + }, + "config": { + "platform": { + "php": "8.2.99" + } + } +} diff --git a/tests/composer.lock b/tests/composer.lock new file mode 100644 index 0000000000..9e0d823870 --- /dev/null +++ b/tests/composer.lock @@ -0,0 +1,2720 @@ +{ + "_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": "868be8e811ed8ef2531495ab93a0d835", + "packages": [], + "packages-dev": [ + { + "name": "brianium/paratest", + "version": "v7.8.4", + "source": { + "type": "git", + "url": "https://github.com/paratestphp/paratest.git", + "reference": "130a9bf0e269ee5f5b320108f794ad03e275cad4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/130a9bf0e269ee5f5b320108f794ad03e275cad4", + "reference": "130a9bf0e269ee5f5b320108f794ad03e275cad4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "fidry/cpu-core-counter": "^1.2.0", + "jean85/pretty-package-versions": "^2.1.1", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "phpunit/php-code-coverage": "^11.0.10", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-timer": "^7.0.1", + "phpunit/phpunit": "^11.5.24", + "sebastian/environment": "^7.2.1", + "symfony/console": "^6.4.22 || ^7.3.0", + "symfony/process": "^6.4.20 || ^7.3.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0.0", + "ext-pcov": "*", + "ext-posix": "*", + "phpstan/phpstan": "^2.1.17", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpstan/phpstan-phpunit": "^2.0.6", + "phpstan/phpstan-strict-rules": "^2.0.4", + "squizlabs/php_codesniffer": "^3.13.2", + "symfony/filesystem": "^6.4.13 || ^7.3.0" + }, + "bin": [ + "bin/paratest", + "bin/paratest_for_phpstorm" + ], + "type": "library", + "autoload": { + "psr-4": { + "ParaTest\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" + } + ], + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", + "keywords": [ + "concurrent", + "parallel", + "phpunit", + "testing" + ], + "support": { + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v7.8.4" + }, + "funding": [ + { + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" + }, + { + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" + } + ], + "time": "2025-06-23T06:07:21+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": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.3", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", + "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.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/e2e/ResultCacheEndToEndTest.php b/tests/e2e/ResultCacheEndToEndTest.php deleted file mode 100644 index 1117af27b0..0000000000 --- a/tests/e2e/ResultCacheEndToEndTest.php +++ /dev/null @@ -1,208 +0,0 @@ -&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->assertIsArray($result['totals']); - $this->assertSame(3, $result['totals']['file_errors']); - $this->assertSame(0, $result['totals']['errors']); - - $fileHelper = new FileHelper(__DIR__); - - $this->assertSame('Parameter #1 $code of function token_get_all expects string, PhpParser\Node\Expr\MethodCall given.', $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/PhpParser/Lexer.php')]['messages'][0]['message']); - $this->assertSame('Parameter #1 $code of method PhpParser\Lexer::startLexing() expects PhpParser\Node\Expr\MethodCall, string given.', $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/PhpParser/ParserAbstract.php')]['messages'][0]['message']); - $this->assertSame('Parameter #1 (array{\'foo\'}) of echo cannot be converted to string.', $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/bootstrap.php')]['messages'][0]['message']); - $this->assertResultCache(__DIR__ . '/resultCache_2.php'); - } - - public function testResultCacheDeleteFile(): void - { - $this->runPhpstan(0); - $this->assertResultCache(__DIR__ . '/resultCache_1.php'); - - $serializerPath = __DIR__ . '/PHP-Parser/lib/PhpParser/Serializer.php'; - $serializerCode = FileReader::read($serializerPath); - $originalSerializerCode = $serializerCode; - unlink($serializerPath); - - $fileHelper = new FileHelper(__DIR__); - - $result = $this->runPhpstan(1); - $this->assertIsArray($result['totals']); - $this->assertSame(1, $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->assertSame('Class PhpParser\\Serializer\\XML implements unknown interface PhpParser\\Serializer.', $message); - - file_put_contents($serializerPath, $originalSerializerCode); - $this->runPhpstan(0); - $this->assertResultCache(__DIR__ . '/resultCache_1.php'); - } - - public function testResultCachePath(): void - { - $this->runPhpstan(0, __DIR__ . '/phpstan_resultcachepath.neon'); - - $this->assertFileExists(sys_get_temp_dir() . '/phpstan/myResultCacheFile.php'); - $this->assertResultCache(__DIR__ . '/resultCache_1.php', sys_get_temp_dir() . '/phpstan/myResultCacheFile.php'); - } - - /** - * @return mixed[] - */ - private function runPhpstan(int $expectedExitCode, string $phpstanConfigPath = __DIR__ . '/phpstan.neon'): 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($phpstanConfigPath), - ), $outputLines, $exitCode); - $output = implode("\n", $outputLines); - - try { - $json = Json::decode($output, Json::FORCE_ARRAY); - $this->assertIsArray($json); - } catch (JsonException $e) { - $this->fail(sprintf('%s: %s', $e->getMessage(), $output)); - } - - if ($exitCode !== $expectedExitCode) { - $this->fail($output); - } - - return $json; - } - - /** - * @param mixed[] $resultCache - * @return array> - */ - private function transformResultCache(array $resultCache): array - { - $new = []; - $this->assertIsArray($resultCache['dependencies']); - foreach ($resultCache['dependencies'] as $file => $data) { - $this->assertIsString($file); - $this->assertIsArray($data); - $this->assertIsArray($data['dependentFiles']); - - $files = []; - foreach ($data['dependentFiles'] as $filePath) { - $this->assertIsString($filePath); - $files[] = $this->relativizePath($filePath); - } - 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, string $actualCachePath = __DIR__ . '/tmp/resultCache.php'): void - { - $resultCache = $this->transformResultCache(require $actualCachePath); - $expectedResultCachePath = require $expectedCachePath; - $this->assertSame($expectedResultCachePath, $resultCache); - } - -} diff --git a/tests/e2e/baseline.neon b/tests/e2e/baseline.neon deleted file mode 100644 index aad958b3b5..0000000000 --- a/tests/e2e/baseline.neon +++ /dev/null @@ -1,172 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^PHPDoc tag @param references unknown parameter\\: \\$interfaces$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Builder/Class_.php - - - - message: "#^Access to an undefined property PhpParser\\\\Node\\:\\:\\$stmts\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Builder/Interface_.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\\\\BuilderAbstract\\:\\:\\$flags\\.$#" - count: 2 - 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: "#^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: "#^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\\\\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: "#^Method PhpParser\\\\NodeVisitorAbstract\\:\\:afterTraverse\\(\\) should return array\\\\|null but return statement is missing\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/NodeVisitorAbstract.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: "#^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: "#^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: "#^Property PhpParser\\\\ParserAbstract\\:\\:\\$endAttributeStack \\(array\\\\) does not accept array\\\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - - - message: "#^Property PhpParser\\\\ParserAbstract\\:\\:\\$endAttributes \\(array\\) does not accept string\\.$#" - 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: "#^Variable \\$tokenValue might not be defined\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/ParserAbstract.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/phpstan.neon b/tests/e2e/phpstan.neon deleted file mode 100644 index b9fe1cd9c1..0000000000 --- a/tests/e2e/phpstan.neon +++ /dev/null @@ -1,7 +0,0 @@ -includes: - - baseline.neon - -parameters: - phpVersion: 80000 - tmpDir: tmp - treatPhpDocTypesAsCertain: false diff --git a/tests/e2e/phpstan_resultcachepath.neon b/tests/e2e/phpstan_resultcachepath.neon deleted file mode 100644 index 3ad1d1a3b1..0000000000 --- a/tests/e2e/phpstan_resultcachepath.neon +++ /dev/null @@ -1,7 +0,0 @@ -includes: - - baseline.neon - -parameters: - phpVersion: 80000 - resultCachePath: %tmpDir%/myResultCacheFile.php - treatPhpDocTypesAsCertain: false diff --git a/tests/e2e/resultCache_1.php b/tests/e2e/resultCache_1.php deleted file mode 100644 index fd17dda8f2..0000000000 --- a/tests/e2e/resultCache_1.php +++ /dev/null @@ -1,2019 +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.php', - 32 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 33 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 34 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 35 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 36 => 'lib/PhpParser/Parser/Php5.php', - 37 => 'lib/PhpParser/Parser/Php7.php', - 38 => 'lib/PhpParser/ParserAbstract.php', - 39 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 40 => '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 ( - ), -); diff --git a/tests/e2e/resultCache_2.php b/tests/e2e/resultCache_2.php deleted file mode 100644 index fb4e9bb6d8..0000000000 --- a/tests/e2e/resultCache_2.php +++ /dev/null @@ -1,2023 +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.php', - 32 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 33 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 34 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 35 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 36 => 'lib/PhpParser/Parser/Php5.php', - 37 => 'lib/PhpParser/Parser/Php7.php', - 38 => 'lib/PhpParser/ParserAbstract.php', - 39 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 40 => '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 ( - ), -); diff --git a/tests/e2e/resultCache_3.php b/tests/e2e/resultCache_3.php deleted file mode 100644 index 993f2b65ee..0000000000 --- a/tests/e2e/resultCache_3.php +++ /dev/null @@ -1,2016 +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.php', - 32 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 33 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 34 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 35 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 36 => 'lib/PhpParser/Parser/Php5.php', - 37 => 'lib/PhpParser/Parser/Php7.php', - 38 => 'lib/PhpParser/ParserAbstract.php', - 39 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 40 => '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/notAutoloaded/nonexistentClasses-error.php b/tests/notAutoloaded/nonexistentClasses-error.php new file mode 100644 index 0000000000..61c09f8bbf --- /dev/null +++ b/tests/notAutoloaded/nonexistentClasses-error.php @@ -0,0 +1,21 @@ + - - - - 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -